Browse Source

Extend mail control functions

- Rework remove functionality
- Add restore mail functionality
- Add mail control panel to mail list
Alexey Edelev 5 years ago
parent
commit
46f1c1b606
12 changed files with 199 additions and 30 deletions
  1. 1 0
      common/metadata.go
  2. 61 13
      db/db.go
  3. 42 0
      web/assets/restore.svg
  4. 7 0
      web/css/controls.css
  5. 10 0
      web/css/index.css
  6. 12 2
      web/css/styles.css
  7. 31 3
      web/js/index.js
  8. 17 5
      web/mail.go
  9. 2 2
      web/mailbox.go
  10. 2 0
      web/server.go
  11. 2 1
      web/templates/details.html
  12. 12 4
      web/templates/maillist.html

+ 1 - 0
common/metadata.go

@@ -31,4 +31,5 @@ type MailMetadata struct {
 	Mail   *Mail
 	Read   bool
 	Folder string
+	Trash  bool
 }

+ 61 - 13
db/db.go

@@ -315,11 +315,13 @@ func (s *Storage) SaveMail(email, folder string, m *common.Mail) error {
 		Mail   *common.Mail
 		Folder string
 		Read   bool
+		Trash  bool
 	}{
 		Email:  email,
 		Mail:   m,
 		Folder: folder,
 		Read:   false,
+		Trash:  false,
 	}, options.InsertOne().SetBypassDocumentValidation(true))
 	return nil
 }
@@ -332,7 +334,23 @@ func (s *Storage) MoveMail(user string, mailId string, folder string) error {
 		return err
 	}
 
-	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"folder": folder}})
+	if folder == common.Trash {
+		_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"trash": true}})
+	} else {
+		_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"folder": folder, "trash": false}})
+	}
+	return err
+}
+
+func (s *Storage) RestoreMail(user string, mailId string, folder string) error {
+	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
+
+	oId, err := primitive.ObjectIDFromHex(mailId)
+	if err != nil {
+		return err
+	}
+
+	_, err = mailsCollection.UpdateOne(context.Background(), bson.M{"_id": oId}, bson.M{"$set": bson.M{"trash": false}})
 	return err
 }
 
@@ -348,11 +366,25 @@ func (s *Storage) DeleteMail(user string, mailId string) error {
 	return err
 }
 
-func (s *Storage) MailList(user, email, folder string, frame common.Frame) ([]*common.MailMetadata, error) {
+func (s *Storage) GetMailList(user, email, folder string, frame common.Frame) ([]*common.MailMetadata, error) {
 	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
 
+	matchFilter := bson.M{"email": email}
+	if folder == common.Trash {
+		matchFilter["$or"] = bson.A{
+			bson.M{"trash": true},
+			bson.M{"folder": folder}, //legacy support
+		}
+	} else {
+		matchFilter["folder"] = folder
+		matchFilter["$or"] = bson.A{
+			bson.M{"trash": false},
+			bson.M{"trash": bson.M{"$exists": false}}, //legacy support
+		}
+	}
+
 	request := bson.A{
-		bson.M{"$match": bson.M{"email": email, "folder": folder}},
+		bson.M{"$match": matchFilter},
 		bson.M{"$sort": bson.M{"mail.header.date": -1}},
 	}
 
@@ -369,6 +401,7 @@ func (s *Storage) MailList(user, email, folder string, frame common.Frame) ([]*c
 	cur, err := mailsCollection.Aggregate(context.Background(), request)
 
 	if err != nil {
+		log.Println(err.Error())
 		return nil, err
 	}
 
@@ -401,14 +434,31 @@ func (s *Storage) GetEmailStats(user string, email string, folder string) (unrea
 		Unread int
 	}{}
 
-	cur, err := mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": bson.M{"email": email, "folder": folder, "read": false}}, bson.M{"$count": "unread"}})
+	matchFilter := bson.M{"email": email}
+	if folder == common.Trash {
+		matchFilter["$or"] = bson.A{
+			bson.M{"trash": true},
+			bson.M{"folder": folder}, //legacy support
+		}
+	} else {
+		matchFilter["folder"] = folder
+		matchFilter["$or"] = bson.A{
+			bson.M{"trash": false},
+			bson.M{"trash": bson.M{"$exists": false}}, //legacy support
+		}
+	}
+
+	unreadMatchFilter := matchFilter
+	unreadMatchFilter["read"] = false
+
+	cur, err := mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": unreadMatchFilter}, bson.M{"$count": "unread"}})
 	if err == nil && cur.Next(context.Background()) {
 		cur.Decode(result)
 	} else {
 		return 0, 0, err
 	}
 
-	cur, err = mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": bson.M{"email": email, "folder": folder}}, bson.M{"$count": "total"}})
+	cur, err = mailsCollection.Aggregate(context.Background(), bson.A{bson.M{"$match": matchFilter}, bson.M{"$count": "total"}})
 	if err == nil && cur.Next(context.Background()) {
 		cur.Decode(result)
 	} else {
@@ -418,7 +468,7 @@ func (s *Storage) GetEmailStats(user string, email string, folder string) (unrea
 	return result.Unread, result.Total, err
 }
 
-func (s *Storage) GetMail(user string, id string) (m *common.Mail, err error) {
+func (s *Storage) GetMail(user string, id string) (metadata *common.MailMetadata, err error) {
 	mailsCollection := s.db.Collection(qualifiedMailCollection(user))
 
 	oId, err := primitive.ObjectIDFromHex(id)
@@ -426,17 +476,15 @@ func (s *Storage) GetMail(user string, id string) (m *common.Mail, err error) {
 		return nil, err
 	}
 
-	m = &common.Mail{}
-	result := &struct {
-		Mail *common.Mail
-	}{
-		Mail: m,
+	metadata = &common.MailMetadata{
+		Mail: &common.Mail{},
 	}
-	err = mailsCollection.FindOne(context.Background(), bson.M{"_id": oId}).Decode(result)
+
+	err = mailsCollection.FindOne(context.Background(), bson.M{"_id": oId}).Decode(metadata)
 	if err != nil {
 		return nil, err
 	}
-	return result.Mail, nil
+	return metadata, nil
 }
 
 func (s *Storage) SetRead(user string, id string, read bool) error {

File diff suppressed because it is too large
+ 42 - 0
web/assets/restore.svg


+ 7 - 0
web/css/controls.css

@@ -236,6 +236,13 @@
 .iconBtn {
     width: 20pt;
     min-width: 20pt;
+
+    -webkit-touch-callout: none;
+    -webkit-user-select: none;
+    -khtml-user-select: none;
+    -moz-user-select: none;
+    -ms-user-select: none;
+    user-select: none;
 }
 
 /* Dropdown Button */

+ 10 - 0
web/css/index.css

@@ -131,6 +131,16 @@ html, body {
     padding: var(--base-text-padding);
 }
 
+.mailControlPanel {
+    position: absolute;
+    min-width: 100pt;
+    top: 0pt;
+    right: 0pt;
+    bottom: 1pt;
+    z-index: 2;
+    display: none;
+}
+
 #copyrightBox {
     flex: 0 1 auto;
     text-overflow: ellipsis;

+ 12 - 2
web/css/styles.css

@@ -22,14 +22,15 @@ body {
     background-color: var(--bg-color);
 }
 
-.mailHeader {
+
+.mailHeaderContainer {
     background-color: var(--bg-color);
     border-bottom: 1px solid var(--primary-color);
     cursor: pointer;
     transition: background-color .3s;
 }
 
-.mailHeader:hover {
+.mailHeaderContainer:hover {
     background-color: var(--secondary-color);
 }
 
@@ -90,4 +91,13 @@ body {
 
 .dropdown-content a:hover, a:focus {
     background-color: var(--bg-dark-color);
+}
+
+.mailControlPanel {
+    background: -moz-linear-gradient(left, rgba(0, 0, 0, 0) 0%, var(--bg-color) 20%, var(--bg-color) 100%); /* FF3.6+ */
+    background: -webkit-gradient(linear, left top, right top, color-stop(0%,rgba(0, 0, 0,0)), color-stop(90%,var(--bg-color)), color-stop(100%,var(--bg-color))); /* Chrome,Safari4+ */
+    background: -webkit-linear-gradient(left, rgba(0, 0, 0, 0) 0%, var(--bg-color) 20%, var(--bg-color) 100%); /* Chrome10+,Safari5.1+ */
+    background: -o-linear-gradient(left, rgba(0, 0, 0, 0) 0%, var(--bg-color) 20%, var(--bg-color) 100%); /* Opera 11.10+ */
+    background: -ms-linear-gradient(left, rgba(0, 0, 0, 0) 0%, var(--bg-color) 20%, var(--bg-color) 100%); /* IE10+ */
+    background: linear-gradient(to right, rgba(0, 0, 0, 0) 0%, var(--bg-color) 20%, var(--bg-color) 100%); /* W3C */
 }

+ 31 - 3
web/js/index.js

@@ -222,12 +222,18 @@ function setRead(mailId, read) {
                 if ($("#readIcon"+mailId)) {
                     $("#readIcon"+mailId).attr("src", "/assets/read.svg")
                 }
+                if ($("#readListIcon"+mailId)) {
+                    $("#readListIcon"+mailId).attr("src", "/assets/read.svg")
+                }
                 $("#mail"+mailId).removeClass("unread")
                 $("#mail"+mailId).addClass("read")
             } else {
                 if ($("#readIcon"+mailId)) {
                     $("#readIcon"+mailId).attr("src", "/assets/unread.svg")
                 }
+                if ($("#readListIcon"+mailId)) {
+                    $("#readListIcon"+mailId).attr("src", "/assets/unread.svg")
+                }
                 $("#mail"+mailId).removeClass("read")
                 $("#mail"+mailId).addClass("unread")
             }
@@ -238,9 +244,9 @@ function setRead(mailId, read) {
     })
 }
 
-function toggleRead(mailId) {
-    if ($("#readIcon"+mailId)) {
-        setRead(mailId, $("#readIcon"+mailId).attr("src") == "/assets/unread.svg")
+function toggleRead(mailId, iconId) {
+    if ($("#"+iconId+mailId)) {
+        setRead(mailId, $("#"+iconId+mailId).attr("src") == "/assets/unread.svg")
     }
 }
 
@@ -262,6 +268,28 @@ function removeMail(mailId, callback) {
     })
 }
 
+
+function restoreMail(mailId, callback) {
+    var url = "/restore"
+    $.ajax({
+        url: url,
+        data: {mailId: mailId},
+        success: function(result) {
+            if (currentFolder == "Trash") {
+                $("#mail"+mailId).remove();
+            }
+            if (callback) {
+                callback();
+            }
+            for (var i = 0; i < folders.length; i++) {
+                folderStat(folders[i])
+            }
+        },
+        error: function(jqXHR, textStatus, errorThrown) {
+        }
+    })
+}
+
 function setDetailsVisible(visible) {
     if (visible) {
         $("#mailDetails").show()

+ 17 - 5
web/mail.go

@@ -56,6 +56,8 @@ func (s *Server) handleMailRequest(w http.ResponseWriter, r *http.Request) {
 		s.handleSetRead(w, r, user, mailId)
 	case "/remove":
 		s.handleRemove(w, user, mailId)
+	case "/restore":
+		s.handleRestore(w, user, mailId)
 	case "/delete":
 		s.handleDelete(w, user, mailId)
 	}
@@ -68,9 +70,9 @@ func (s *Server) handleMailDetails(w http.ResponseWriter, user, mailId string) {
 		return
 	}
 
-	text := mail.Body.RichText
+	text := mail.Mail.Body.RichText
 	if text == "" {
-		text = strings.Replace(mail.Body.PlainText, "\n", "</br>", -1)
+		text = strings.Replace(mail.Mail.Body.PlainText, "\n", "</br>", -1)
 	}
 
 	s.storage.SetRead(user, mailId, true)
@@ -81,12 +83,15 @@ func (s *Server) handleMailDetails(w http.ResponseWriter, user, mailId string) {
 		Text    template.HTML
 		MailId  string
 		Read    bool
+		Trash   bool
 	}{
-		From:    mail.Header.From,
-		To:      mail.Header.To,
-		Subject: mail.Header.Subject,
+		From:    mail.Mail.Header.From,
+		To:      mail.Mail.Header.To,
+		Subject: mail.Mail.Header.Subject,
 		Text:    template.HTML(text),
 		MailId:  mailId,
+		Read:    false,
+		Trash:   mail.Trash,
 	}))
 }
 
@@ -104,6 +109,13 @@ func (s *Server) handleRemove(w http.ResponseWriter, user, mailId string) {
 	}
 }
 
+func (s *Server) handleRestore(w http.ResponseWriter, user, mailId string) {
+	err := s.storage.RestoreMail(user, mailId, common.Trash)
+	if err != nil {
+		s.error(http.StatusInternalServerError, "Could not move email to trash", w)
+	}
+}
+
 func (s *Server) handleDelete(w http.ResponseWriter, user, mailId string) {
 	log.Printf("Delete mail")
 	err := s.storage.DeleteMail(user, mailId)

+ 2 - 2
web/mailbox.go

@@ -40,7 +40,7 @@ import (
 )
 
 func (s *Server) handleMailbox(w http.ResponseWriter, user, email string) {
-	mailList, err := s.storage.MailList(user, email, common.Inbox, common.Frame{Skip: 0, Limit: 50})
+	mailList, err := s.storage.GetMailList(user, email, common.Inbox, common.Frame{Skip: 0, Limit: 50})
 
 	if err != nil {
 		s.error(http.StatusInternalServerError, "Couldn't read email database", w)
@@ -147,7 +147,7 @@ func (s *Server) handleMailList(w http.ResponseWriter, r *http.Request, user, em
 		return
 	}
 
-	mailList, err := s.storage.MailList(user, email, folder, common.Frame{Skip: int32(50 * page), Limit: 50})
+	mailList, err := s.storage.GetMailList(user, email, folder, common.Frame{Skip: int32(50 * page), Limit: 50})
 
 	if err != nil {
 		s.error(http.StatusInternalServerError, "Couldn't read email database", w)

+ 2 - 0
web/server.go

@@ -132,6 +132,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 			fallthrough
 		case "/remove":
 			fallthrough
+		case "/restore":
+			fallthrough
 		case "/delete":
 			s.handleMailRequest(w, r)
 		default:

+ 2 - 1
web/templates/details.html

@@ -6,7 +6,8 @@
                 <span class="secondaryText">To: {{.To}}</span></br>
                 <span class="primaryText">Subject: {{.Subject}}</span></br>
             </div>
-            <img id="readIcon{{.MailId}}" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="toggleRead({{.MailId}});" src="/assets/read.svg"/>
+            <img id="readIcon{{.MailId}}" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="toggleRead('{{.MailId}}', 'readIcon');" src="/assets/read.svg"/>
+            <img id="restoreIcon" class="iconBtn" style="display:{{if .Trash}}block{{else}}none{{end}}; width: 20pt; margin-right: 10pt;" onclick="restoreMail({{.MailId}}, closeDetails);" src="/assets/restore.svg"/>
             <img id="deleteIcon" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="removeMail({{.MailId}}, closeDetails);" src="/assets/remove.svg"/>
             <div class="btn materialLevel1" style="width: 40pt; right: 0pt; top: 0pt; margin: auto;" onclick="closeDetails();">
                 <img src="/assets/back.svg" style="width: 20pt"/>

+ 12 - 4
web/templates/maillist.html

@@ -1,9 +1,17 @@
 <!-- <div class="fadeIn" style="position: absolute; top: 5pt; left: 0; right: 0; height: 10pt"></div> -->
 {{range .}}
-<div id="mail{{.Id}}" class="mailHeader noselect {{if .Read}}read{{else}}unread{{end}}" onclick="openEmail('{{.Id}}');">
-    <div class="mailFrom noselect">{{.Mail.Header.From}}</div>
-    <div class="mailSubject noselect">{{.Mail.Header.Subject}}</div>
-    <div id="mailDate{{.Id}}" class="mailDate noselect" onload="$('#mailDate{{.Id}}').html(localDate({{.Mail.Header.Date}}))">{{.Mail.Header.Subject}}</div>
+<div id="mail{{.Id}}" class="mailHeaderContainer {{if .Read}}read{{else}}unread{{end}}" style="position: relative;" onmouseover="$('#mailControlPanel{{.Id}}').show()" onmouseout="$('#mailControlPanel{{.Id}}').hide()">
+    <div class="mailHeader noselect" onclick="openEmail('{{.Id}}');" >
+        <div class="mailFrom noselect">{{.Mail.Header.From}}</div>
+        <div class="mailSubject noselect">{{.Mail.Header.Subject}}</div>
+        <div id="mailDate{{.Id}}" class="mailDate noselect" onload="$('#mailDate{{.Id}}').html(localDate({{.Mail.Header.Date}}))">{{.Mail.Header.Subject}}</div>
+    </div>
+    <div id="mailControlPanel{{.Id}}" class="mailControlPanel">
+        <div style="width: 100%; height: 100%; display: flex; flex-direction: row;">
+            <img id="readListIcon{{.Id}}" class="iconBtn" style="width: 20pt; margin-left: 20pt; margin-right: 10pt;" onclick="toggleRead('{{.Id}}', 'readListIcon'); event.stopPropagation(); console.log(event); return false;" src="/assets/{{if .Read}}read{{else}}unread{{end}}.svg"/>
+            <img id="deleteListIcon" class="iconBtn" style="width: 20pt; margin-right: 10pt;" onclick="removeMail({{.Id}}, closeDetails); event.stopPropagation(); return false;" src="/assets/remove.svg"/>
+        </div>
+    </div>
 </div>
 {{end}}
 <!-- <div class="fadeOut" style="position: absolute; bottom: 5pt; left: 0; right: 0; height:10pt"></div> -->

Some files were not shown because too many files changed in this diff