diff --git a/README.md b/README.md index 835153b..fd84f91 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,6 @@ ax create "Task" --type issue --status open --prio high | `_status` | `open`, `done` | No | | `_prio` | `high`, `medium`, `low` | No | | `_namespace` | any string | Yes (default: current user) | -| `_inbox` | username | No (auto-set from @mentions) | ## Mentions and Inbox diff --git a/cmd/inbox.go b/cmd/inbox.go index fae6530..066b1f4 100644 --- a/cmd/inbox.go +++ b/cmd/inbox.go @@ -17,7 +17,16 @@ var inboxCmd = &cobra.Command{ fmt.Fprintln(os.Stderr, err) return } - if nodes, err := d.ListNodes(db.ListFilter{TagPrefixes: []string{"_inbox::" + db.GetCurrentUser()}}); err == nil { + userID, err := d.GetUserByUsername(db.GetCurrentUser()) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return + } + if userID == "" { + output.PrintNodes(cmd.OutOrStdout(), nil, jsonFlag) + return + } + if nodes, err := d.ListNodes(db.ListFilter{MentionsUser: userID}); err == nil { output.PrintNodes(cmd.OutOrStdout(), nodes, jsonFlag) } }, diff --git a/db/node.go b/db/node.go index 1ec7991..6960cc0 100644 --- a/db/node.go +++ b/db/node.go @@ -67,6 +67,18 @@ func (db *DB) resolveUserRef(tx *sql.Tx, ref string) (string, error) { return db.ensureUser(tx, ref) } +func (db *DB) GetUserByUsername(username string) (string, error) { + var id string + err := db.QueryRow(` + SELECT n.id FROM nodes n + JOIN tags t ON n.id = t.node_id + WHERE n.title = ? AND t.tag = '_type::user'`, username).Scan(&id) + if err == sql.ErrNoRows { + return "", nil + } + return id, err +} + func (db *DB) ensureNamespace(tx *sql.Tx, name string) (string, error) { var existingID string err := tx.QueryRow(` @@ -123,8 +135,9 @@ type UpdateParams struct { AddRels, RemoveRels map[models.RelType][]string } type ListFilter struct { - TagPrefixes []string - Assignee string + TagPrefixes []string + Assignee string + MentionsUser string } func (db *DB) CreateNode(p CreateParams) (*models.Node, error) { @@ -142,17 +155,17 @@ func (db *DB) CreateNode(p CreateParams) (*models.Node, error) { } for _, m := range parse.Mentions(p.Title + " " + p.Content) { - if _, err := db.ensureUser(tx, m); err != nil { + userID, err := db.resolveUserRef(tx, m) + if err != nil { + return nil, err + } + if _, err := tx.Exec("INSERT INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, userID, models.RelMentions); err != nil { return nil, err } } - for _, t := range append(p.Tags, parse.Mentions(p.Title+" "+p.Content)...) { - if !strings.HasPrefix(t, "_") && strings.HasPrefix(t, "@") { - if _, err = tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, "_inbox::"+t[1:]); err != nil { - return nil, err - } - } else if _, err := tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, t); err != nil { + for _, t := range p.Tags { + if _, err := tx.Exec("INSERT INTO tags (node_id, tag) VALUES (?, ?)", id, t); err != nil { return nil, err } } @@ -188,24 +201,28 @@ func (db *DB) UpdateNode(id string, p UpdateParams) error { } defer tx.Rollback() + var currentTitle, currentContent string + err = tx.QueryRow("SELECT title, COALESCE(content, '') FROM nodes WHERE id = ?", id).Scan(¤tTitle, ¤tContent) + if err != nil { + return err + } + upd := func(col, val string) error { _, err := tx.Exec("UPDATE nodes SET "+col+" = ? WHERE id = ?", val, id) return err } + newTitle, newContent := currentTitle, currentContent if p.Title != nil { if err := upd("title", *p.Title); err != nil { return err } + newTitle = *p.Title } if p.Content != nil { if err := upd("content", *p.Content); err != nil { return err } - for _, m := range parse.Mentions(*p.Content) { - if _, err := db.ensureUser(tx, m); err != nil { - return err - } - } + newContent = *p.Content } if p.DueDate != nil { if err := upd("due_date", *p.DueDate); err != nil { @@ -218,6 +235,46 @@ func (db *DB) UpdateNode(id string, p UpdateParams) error { } } + if p.Title != nil || p.Content != nil { + newMentions := parse.Mentions(newTitle + " " + newContent) + rows, err := tx.Query("SELECT to_id FROM rels WHERE from_id = ? AND rel_type = ?", id, models.RelMentions) + if err != nil { + return err + } + existingMentionIDs := make(map[string]bool) + for rows.Next() { + var uid string + if err := rows.Scan(&uid); err != nil { + rows.Close() + return err + } + existingMentionIDs[uid] = true + } + rows.Close() + + mentionedUserIDs := make(map[string]bool) + for _, m := range newMentions { + userID, err := db.resolveUserRef(tx, m) + if err != nil { + return err + } + mentionedUserIDs[userID] = true + if !existingMentionIDs[userID] { + if _, err := tx.Exec("INSERT INTO rels (from_id, to_id, rel_type) VALUES (?, ?, ?)", id, userID, models.RelMentions); err != nil { + return err + } + } + } + + for uid := range existingMentionIDs { + if !mentionedUserIDs[uid] { + if _, err := tx.Exec("DELETE FROM rels WHERE from_id = ? AND to_id = ? AND rel_type = ?", id, uid, models.RelMentions); err != nil { + return err + } + } + } + } + for _, t := range p.RemoveTags { tx.Exec("DELETE FROM tags WHERE node_id = ? AND tag = ?", id, t) } @@ -315,6 +372,11 @@ func (db *DB) ListNodes(f ListFilter) ([]*models.Node, error) { conds = append(conds, "r_assign.to_id = ? AND r_assign.rel_type = ?") args = append(args, f.Assignee, models.RelAssignee) } + if f.MentionsUser != "" { + joins = append(joins, "JOIN rels r_mentions ON n.id = r_mentions.from_id") + conds = append(conds, "r_mentions.to_id = ? AND r_mentions.rel_type = ?") + args = append(args, f.MentionsUser, models.RelMentions) + } if len(joins) > 0 { q += " " + strings.Join(joins, " ") + " " diff --git a/models/node.go b/models/node.go index 2ee8aac..0c17883 100644 --- a/models/node.go +++ b/models/node.go @@ -22,6 +22,7 @@ const ( RelCreated RelType = "created" RelAssignee RelType = "assignee" RelInNamespace RelType = "in_namespace" + RelMentions RelType = "mentions" ) func (n *Node) GetProperty(k string) string {