diff --git a/internal/api_handler.go b/internal/api_handler.go
index 3830ecdb..52648bf7 100644
--- a/internal/api_handler.go
+++ b/internal/api_handler.go
@@ -20,6 +20,12 @@ func apiHandler(e *echo.Echo) {
e.POST("/web/article/saveChanges", post.SaveArticleChanges, session.CheckSignIn)
// comment apis
- e.POST("/web/post/comment", post.Comment, session.CheckSignIn)
- e.GET("/web/post/queryComments", post.QueryComments)
+ e.POST("/web/comment/create", post.Comment, session.CheckSignIn)
+ e.POST("/web/comment/reply", post.CommentReply, session.CheckSignIn)
+ e.POST("/web/comment/edit", post.EditComment, session.CheckSignIn)
+ e.POST("/web/comment/delete", post.DeleteComment, session.CheckSignIn)
+ e.POST("/web/comment/revert", post.RevertComment, session.CheckSignIn)
+ e.GET("/web/comment/query", post.QueryComments)
+ e.POST("/web/comment/like", post.CommentLike, session.CheckSignIn)
+ e.POST("/web/comment/dislike", post.CommentDislike, session.CheckSignIn)
}
diff --git a/internal/ecode/ecode.go b/internal/ecode/ecode.go
index 2243638b..43cd40e2 100644
--- a/internal/ecode/ecode.go
+++ b/internal/ecode/ecode.go
@@ -23,3 +23,9 @@ const (
PostNotFound = 1101
PostNotFoundMsg = "Target post not found"
)
+
+// comment
+const (
+ CommentLiked = 1200
+ CommentLikedMsg = "You have agreed this comment before"
+)
diff --git a/internal/post/comment.go b/internal/post/comment.go
index 02fac510..313bfbd8 100644
--- a/internal/post/comment.go
+++ b/internal/post/comment.go
@@ -2,11 +2,14 @@ package post
import (
"encoding/json"
+ "fmt"
"net/http"
"sort"
"strconv"
+ "strings"
"time"
+ "github.com/gocql/gocql"
"github.com/labstack/echo"
"github.com/thinkindev/im.dev/internal/ecode"
"github.com/thinkindev/im.dev/internal/misc"
@@ -18,14 +21,23 @@ import (
// CommentContent holds the comment content
type CommentContent struct {
ID string `json:"id"`
+ PID string `json:"pid"`
+ Depth int `json:"depth"`
MD string `json:"md"`
Render string `json:"render"`
UID string `json:"uid"`
UName string `json:"uname"`
UNickname string `json:"unickname"`
- UAvatar string `json:"uavatar"`
PublishDate string `json:"date"`
publishDate int64
+
+ EditDate string `json:"edit_date,omitempty"`
+ editDate int64
+
+ Likes int `json:"likes"`
+ Liked int `json:"liked"` // current login user's liked to the comment, 0 normal, 1 liked, 2 disliked
+
+ Status int `json:"status"`
}
// Comments is the list of comment
@@ -38,7 +50,7 @@ func (a Comments) Swap(i, j int) { // 重写 Swap() 方法
a[i], a[j] = a[j], a[i]
}
func (a Comments) Less(i, j int) bool { // 重写 Less() 方法, 从小到大排序
- return a[i].publishDate < a[j].publishDate
+ return a[i].publishDate >= a[j].publishDate
}
// Comment is the action that user make commention to one post
@@ -89,11 +101,15 @@ func Comment(c echo.Context) error {
cc.UID = sess.ID
// generate id for article
cc.ID = misc.GenID()
+ cc.UName = sess.Name
+ cc.UNickname = sess.NickName
// modify render
cc.Render = modify(cc.Render)
+ cc.publishDate = time.Now().Unix()
+ cc.PublishDate = utils.Time2ReadableString(time.Unix(cc.publishDate, 0))
err = misc.CQL.Query("INSERT INTO comment (id,uid,post_id,post_type,md,render,publish_date) VALUES (?,?,?,?,?,?,?)",
- cc.ID, sess.ID, postID, postType, cc.MD, cc.Render, time.Now().Unix()).Exec()
+ cc.ID, sess.ID, postID, postType, cc.MD, cc.Render, cc.publishDate).Exec()
if err != nil {
misc.Log.Warn("access database error", zap.Error(err))
return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
@@ -116,6 +132,249 @@ func Comment(c echo.Context) error {
})
}
+// CommentReply is the action that user make commention to one post
+func CommentReply(c echo.Context) error {
+ pid := c.FormValue("pid")
+ postID := c.FormValue("post_id")
+ postType, _ := strconv.Atoi(c.FormValue("post_type"))
+ content := c.FormValue("content")
+
+ if pid == "" || postID == "" || postType != ArticleType || content == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ // check whether target post exist
+ var title string
+ var err error
+ switch postType {
+ case ArticleType:
+ err = misc.CQL.Query("SELECT title FROM article WHERE id=?", postID).Scan(&title)
+ }
+ if err != nil {
+ if err.Error() == misc.CQLNotFound {
+ return c.JSON(http.StatusNotFound, misc.HTTPResp{
+ ErrCode: ecode.PostNotFound,
+ Message: ecode.PostNotFoundMsg,
+ })
+ }
+ misc.Log.Warn("access database error", zap.Error(err))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ cc := &CommentContent{}
+ err = json.Unmarshal([]byte(content), &cc)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+
+ cc.UID = sess.ID
+ // generate id for article
+ cc.ID = misc.GenID()
+ cc.UName = sess.Name
+ cc.UNickname = sess.NickName
+ // modify render
+ cc.Render = modify(cc.Render)
+ cc.publishDate = time.Now().Unix()
+ cc.PublishDate = utils.Time2ReadableString(time.Unix(cc.publishDate, 0))
+ cc.PID = pid
+ err = misc.CQL.Query("INSERT INTO comment (id,pid,uid,post_id,post_type,md,render,publish_date) VALUES (?,?,?,?,?,?,?,?)",
+ cc.ID, pid, sess.ID, postID, postType, cc.MD, cc.Render, cc.publishDate).Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ // update post comment count
+ switch postType {
+ case ArticleType:
+ err = misc.CQL.Query("UPDATE post_counter SET comments=comments + 1 WHERE id=?", postID).Exec()
+ }
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err))
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{
+ Data: cc,
+ })
+}
+
+// EditComment edit the comment
+func EditComment(c echo.Context) error {
+ id := c.FormValue("id")
+ content := c.FormValue("content")
+
+ if id == "" || content == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+
+ // check permission
+ q := misc.CQL.Query(`SELECT uid FROM comment WHERE id=?`, id)
+ var uid string
+ err := q.Scan(&uid)
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+ if uid != sess.ID {
+ return c.JSON(http.StatusUnauthorized, misc.HTTPResp{
+ ErrCode: ecode.NoPermission,
+ Message: ecode.NoPermissionMsg,
+ })
+ }
+
+ cc := &CommentContent{}
+ err = json.Unmarshal([]byte(content), &cc)
+ if err != nil {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ cc.Render = modify(cc.Render)
+
+ q = misc.CQL.Query(`UPDATE comment SET md=?,render=?,edit_date=? WHERE id=?`, cc.MD, cc.Render, time.Now().Unix(), id)
+ err = q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{
+ Data: cc.Render,
+ })
+}
+
+// DeleteComment delete the comment
+// Only comment author can do this
+func DeleteComment(c echo.Context) error {
+ id := c.FormValue("id")
+ if id == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+ // check comment exists and this user has permission
+ var uid string
+ q := misc.CQL.Query(`SELECT uid FROM comment WHERE id=?`, id)
+ err := q.Scan(&uid)
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ if sess.ID != uid {
+ return c.JSON(http.StatusUnauthorized, misc.HTTPResp{
+ ErrCode: ecode.NoPermission,
+ Message: ecode.NoPermissionMsg,
+ })
+ }
+
+ // set comment to delete status
+ q = misc.CQL.Query(`UPDATE comment SET status=? WHERE id=?`, StatusDeleted, id)
+ err = q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{})
+}
+
+// RevertComment revert the comment from delete status
+// Only comment owner and post author can do this
+func RevertComment(c echo.Context) error {
+ id := c.FormValue("id")
+ if id == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+ // check comment exists and this user has permission
+ var uid, md, render string
+
+ q := misc.CQL.Query(`SELECT uid,md,render FROM comment WHERE id=?`, id)
+ err := q.Scan(&uid, &md, &render)
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ if sess.ID != uid {
+ return c.JSON(http.StatusUnauthorized, misc.HTTPResp{
+ ErrCode: ecode.NoPermission,
+ Message: ecode.NoPermissionMsg,
+ })
+ }
+
+ // set comment to delete status
+ q = misc.CQL.Query(`UPDATE comment SET status=? WHERE id=?`, StatusNormal, id)
+ err = q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ comment := &CommentContent{}
+ comment.MD = md
+ comment.Render = render
+ u := session.GetUserByID(uid)
+ if u == nil {
+ comment.UName = "[404]"
+ comment.UNickname = "[404]"
+ } else {
+ comment.UName = u.Name
+ comment.UNickname = u.NickName
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{
+ Data: comment,
+ })
+}
+
// QueryComments return the comments by post id
func QueryComments(c echo.Context) error {
postID := c.FormValue("post_id")
@@ -126,34 +385,61 @@ func QueryComments(c echo.Context) error {
})
}
- q := misc.CQL.Query(`SELECT id,uid,md,render,publish_date FROM comment WHERE post_id=?`, postID)
+ q := misc.CQL.Query(`SELECT id,pid,uid,md,render,publish_date,edit_date,status FROM comment WHERE post_id=?`, postID)
iter := q.Iter()
- comments := make(Comments, 0)
- var cid, uid, md, render string
- var pdate int64
- for iter.Scan(&cid, &uid, &md, &render, &pdate) {
+ // comment map
+ cMap := make(map[string]*CommentContent)
+ // parent -> child map
+ pMap := make(map[string][]string)
+ // no parent list
+ nopList := make([]string, 0)
+
+ var cid, uid, md, render, pid string
+ var pdate, edate int64
+ var status int
+ for iter.Scan(&cid, &pid, &uid, &md, &render, &pdate, &edate, &status) {
comment := &CommentContent{
ID: cid,
+ PID: pid,
UID: uid,
MD: md,
Render: render,
publishDate: pdate,
+ editDate: edate,
+ Status: status,
}
u := session.GetUserByID(comment.UID)
if u == nil {
continue
}
- comment.UName = u.Name
- comment.UNickname = u.NickName
- comment.UAvatar = u.Avatar
- comment.PublishDate = utils.Time2ReadableString(time.Unix(comment.publishDate, 0))
+ if status == StatusDeleted {
+ comment.MD = "[deleted]"
+ comment.Render = "[deleted]"
+ comment.UName = "[deleted]"
+ comment.UNickname = "[deleted]"
+ } else {
+ comment.UName = u.Name
+ comment.UNickname = u.NickName
+ }
- comments = append(comments, comment)
+ comment.PublishDate = utils.Time2ReadableString(time.Unix(comment.publishDate, 0))
+ if comment.editDate != 0 {
+ comment.EditDate = utils.Time2ReadableString(time.Unix(comment.editDate, 0))
+ }
+ cMap[comment.ID] = comment
+ if comment.PID == "" {
+ nopList = append(nopList, comment.ID)
+ } else {
+ childs, ok := pMap[comment.PID]
+ if !ok {
+ pMap[comment.PID] = []string{comment.ID}
+ } else {
+ pMap[comment.PID] = append(childs, comment.ID)
+ }
+ }
}
- sort.Sort(comments)
-
if err := iter.Close(); err != nil {
misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
@@ -162,7 +448,241 @@ func QueryComments(c echo.Context) error {
})
}
+ // defualt sort based on time ascending
+ sort.Strings(nopList)
+ for _, childs := range pMap {
+ sort.Strings(childs)
+ }
+
+ comments := make([]*CommentContent, 0)
+
+ for _, nop := range nopList {
+ // add first level comment to final list
+ comment := cMap[nop]
+ comment.Depth = 0
+ comments = append(comments, comment)
+
+ // recursively find his childs
+ findCommentChilds(&comments, nop, pMap, cMap, 1)
+ }
+
+ var b strings.Builder
+ b.WriteString(`SELECT id,likes FROM comment_counter WHERE id in (`)
+
+ var b1 strings.Builder
+ sess := session.Get(c)
+ if sess != nil {
+ b1.WriteString(`SELECT comment_id,type FROM comment_like WHERE uid=? and comment_id in (`)
+ }
+ for i, c := range comments {
+ if i == len(comments)-1 {
+ b.WriteString(fmt.Sprintf("'%s')", c.ID))
+ if sess != nil {
+ b1.WriteString(fmt.Sprintf("'%s')", c.ID))
+ }
+ } else {
+ b.WriteString(fmt.Sprintf("'%s',", c.ID))
+ if sess != nil {
+ b1.WriteString(fmt.Sprintf("'%s',", c.ID))
+ }
+ }
+ }
+
+ if len(comments) != 0 {
+ q = misc.CQL.Query(b.String())
+ iter = q.Iter()
+
+ var id string
+ var likes int
+ likesMap := make(map[string]int)
+ for iter.Scan(&id, &likes) {
+ likesMap[id] = likes
+ }
+
+ if err := iter.Close(); err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ likedMap := make(map[string]int)
+ if sess != nil {
+ q := misc.CQL.Query(b1.String(), sess.ID)
+ iter := q.Iter()
+ var liked int
+ var postID string
+ for iter.Scan(&postID, &liked) {
+ likedMap[postID] = liked
+ }
+
+ if err := iter.Close(); err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+ }
+ for _, c := range comments {
+ c.Likes = likesMap[c.ID]
+ if sess != nil {
+ c.Liked = likedMap[c.ID]
+ }
+ }
+ }
+
return c.JSON(http.StatusOK, misc.HTTPResp{
Data: comments,
})
}
+
+func findCommentChilds(comments *([]*CommentContent), pid string, pMap map[string][]string, cMap map[string]*CommentContent, depth int) {
+ childs, ok := pMap[pid]
+ if !ok {
+ return
+ }
+
+ for _, child := range childs {
+ comment := cMap[child]
+ comment.Depth = depth
+ *comments = append(*comments, comment)
+ // findCommentChilds
+ findCommentChilds(comments, child, pMap, cMap, depth+1)
+ }
+}
+
+// CommentLike indicates that a user like/dislike a Comment
+func CommentLike(c echo.Context) error {
+ postID := c.FormValue("id")
+ if postID == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+
+ // check whether you already liked this comment
+ status, err := commentLikeStatus(postID, sess.ID)
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ if status == OpCommentLike {
+ err = commentLike(postID, sess.ID, OpCommentLike, OpDelete)
+ } else {
+ err = commentLike(postID, sess.ID, OpCommentLike, OpUpdate)
+ }
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ var q *gocql.Query
+ if status == OpCommentLike { // from like to normal, -1
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-1 WHERE id=?`, postID)
+ } else if status == OpCommentDislike { // from dislike to like , + 2
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+2 WHERE id=?`, postID)
+ } else { // from normal to like, + 1
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+1 WHERE id=?`, postID)
+ }
+
+ err = q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{})
+}
+
+// CommentDislike indicates that a user dislike a Comment
+func CommentDislike(c echo.Context) error {
+ postID := c.FormValue("id")
+ if postID == "" {
+ return c.JSON(http.StatusBadRequest, misc.HTTPResp{
+ ErrCode: ecode.ParamInvalid,
+ Message: ecode.ParamInvalidMsg,
+ })
+ }
+
+ sess := session.Get(c)
+
+ // check whether you already liked this comment
+ status, err := commentLikeStatus(postID, sess.ID)
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ if status == OpCommentDislike {
+ err = commentLike(postID, sess.ID, OpCommentDislike, OpDelete)
+ } else {
+ err = commentLike(postID, sess.ID, OpCommentDislike, OpUpdate)
+ }
+
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, misc.HTTPResp{
+ ErrCode: ecode.DatabaseError,
+ Message: ecode.CommonErrorMsg,
+ })
+ }
+
+ var q *gocql.Query
+ if status == OpCommentLike { // from like to dislike, -2
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-2 WHERE id=?`, postID)
+ } else if status == OpCommentDislike { // from dislike to normal + 1
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes+1 WHERE id=?`, postID)
+ } else { // from normal to dislike -1
+ q = misc.CQL.Query(`UPDATE comment_counter SET likes=likes-1 WHERE id=?`, postID)
+ }
+
+ err = q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ }
+
+ return c.JSON(http.StatusOK, misc.HTTPResp{})
+}
+
+func commentLikeStatus(id string, uid string) (int, error) {
+ var tp int
+ q := misc.CQL.Query(`SELECT type FROM comment_like WHERE uid=? and comment_id=?`, uid, id)
+ err := q.Scan(&tp)
+ if err != nil {
+ if err.Error() == misc.CQLNotFound {
+ return 0, nil
+ }
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return 0, err
+ }
+ return tp, nil
+}
+
+// postLike is the action that a user click like or dislike on a post/comment
+func commentLike(id string, uid string, tp int, op int) error {
+ var q *gocql.Query
+ switch op {
+ case OpUpdate:
+ q = misc.CQL.Query(`UPDATE comment_like SET type=?,input_date=? WHERE uid=? and comment_id=?`, tp, time.Now().Unix(), uid, id)
+ case OpDelete:
+ q = misc.CQL.Query(`DELETE FROM comment_like WHERE uid=? and comment_id=?`, uid, id)
+ }
+
+ err := q.Exec()
+ if err != nil {
+ misc.Log.Warn("access database error", zap.Error(err), zap.String("query", q.String()))
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/post/const.go b/internal/post/const.go
index bfccf642..f9988b0e 100644
--- a/internal/post/const.go
+++ b/internal/post/const.go
@@ -13,3 +13,24 @@ const (
// ArticleType means the post is an ariticle
ArticleType = 1
)
+
+const (
+ // OpCommentLike means a user like a post
+ OpCommentLike = 1
+ // OpCommentDislike means a user dislike a post
+ OpCommentDislike = 2
+)
+
+const (
+ // OpUpdate is the update operation
+ OpUpdate = 1
+ // OpDelete is the delete operation
+ OpDelete = 2
+)
+
+const (
+ // StatusNormal means post is in normal status
+ StatusNormal = 0
+ // StatusDeleted means post is deleted
+ StatusDeleted = 1
+)
diff --git a/internal/post/modify.go b/internal/post/modify.go
new file mode 100644
index 00000000..aba4b32d
--- /dev/null
+++ b/internal/post/modify.go
@@ -0,0 +1,75 @@
+package post
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/labstack/echo"
+ "github.com/microcosm-cc/bluemonday"
+ "github.com/thinkindev/im.dev/internal/misc"
+ "github.com/thinkindev/im.dev/internal/session"
+ "github.com/thinkindev/im.dev/internal/utils"
+)
+
+// Preview return the new review html of article
+func Preview(c echo.Context) error {
+ render := c.FormValue("render")
+
+ newr := modify(render)
+ return c.JSON(http.StatusOK, misc.HTTPResp{
+ Data: newr,
+ })
+}
+
+/* modify the post content*/
+
+// every user input need to be modified
+// @user -> @user
+// remove js,iframe such html tags and attributes
+func modify(s string) string {
+ p := bluemonday.UGCPolicy()
+ p.AllowAttrs("class").Globally()
+ p.AllowAttrs("id").Globally()
+ p.AllowElements("input")
+ p.AllowAttrs("checked").OnElements("input")
+ p.AllowAttrs("disabled").OnElements("input")
+ p.AllowAttrs("type").OnElements("input")
+ p.AllowAttrs("style").OnElements("span")
+ p.AllowAttrs("style").OnElements("td")
+ p.AllowAttrs("style").OnElements("th")
+ // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines
+ render := p.Sanitize(s)
+
+ afterRender := make([]rune, 0, len(render))
+ idParseFlag := false
+ tempName := make([]rune, 0)
+ for _, r := range render {
+ if r == '@' {
+ idParseFlag = true
+ afterRender = append(afterRender, r)
+ continue
+ }
+ if idParseFlag {
+ if utils.ValidNameRune(r) {
+ tempName = append(tempName, r)
+ } else {
+ // end flag for parse name
+ idParseFlag = false
+
+ // check name exist
+ if session.CheckUserExist(string(tempName)) {
+ // converse @name -> @user
+ afterRender = append(afterRender, []rune(fmt.Sprintf("%s", string(tempName), string(tempName)))...)
+ } else {
+ afterRender = append(afterRender, tempName...)
+ }
+
+ afterRender = append(afterRender, r)
+ }
+ continue
+ }
+
+ afterRender = append(afterRender, r)
+ }
+ return string(afterRender)
+}
diff --git a/internal/post/post.go b/internal/post/post.go
index 3c7f241a..235520f8 100644
--- a/internal/post/post.go
+++ b/internal/post/post.go
@@ -1,67 +1 @@
package post
-
-import (
- "fmt"
- "net/http"
-
- "github.com/labstack/echo"
- "github.com/microcosm-cc/bluemonday"
- "github.com/thinkindev/im.dev/internal/misc"
- "github.com/thinkindev/im.dev/internal/session"
- "github.com/thinkindev/im.dev/internal/utils"
-)
-
-// Preview return the new review html of article
-func Preview(c echo.Context) error {
- render := c.FormValue("render")
-
- newr := modify(render)
- return c.JSON(http.StatusOK, misc.HTTPResp{
- Data: newr,
- })
-}
-
-/* modify the post content*/
-
-// every user input need to be modified
-// @user -> @user
-// remove js,iframe such html tags and attributes
-func modify(s string) string {
- p := bluemonday.UGCPolicy()
- p.AllowAttrs("class").Globally()
- // The policy can then be used to sanitize lots of input and it is safe to use the policy in multiple goroutines
- render := p.Sanitize(s)
-
- afterRender := make([]rune, 0, len(render))
- idParseFlag := false
- tempName := make([]rune, 0)
- for _, r := range render {
- if r == '@' {
- idParseFlag = true
- afterRender = append(afterRender, r)
- continue
- }
- if idParseFlag {
- if utils.ValidNameRune(r) {
- tempName = append(tempName, r)
- } else {
- // end flag for parse name
- idParseFlag = false
-
- // check name exist
- if session.CheckUserExist(string(tempName)) {
- // converse @name -> @user
- afterRender = append(afterRender, []rune(fmt.Sprintf("%s", string(tempName), string(tempName)))...)
- } else {
- afterRender = append(afterRender, tempName...)
- }
-
- afterRender = append(afterRender, r)
- }
- continue
- }
-
- afterRender = append(afterRender, r)
- }
- return string(afterRender)
-}
diff --git a/internal/utils/time.go b/internal/utils/time.go
index a460a67a..7e0c731d 100644
--- a/internal/utils/time.go
+++ b/internal/utils/time.go
@@ -18,15 +18,15 @@ func Time2ReadableString(t time.Time) string {
now := time.Now().Local()
intv := now.Unix() - t.Unix()
if intv < 60 {
- return strconv.FormatInt(intv, 10) + "seconds ago"
+ return strconv.FormatInt(intv, 10) + " seconds ago"
}
if intv < 3600 {
- return strconv.FormatInt(intv/60, 10) + "minutes ago"
+ return strconv.FormatInt(intv/60, 10) + " minutes ago"
}
if intv < 86400 {
- return strconv.FormatInt(intv/3600, 10) + "hours ago"
+ return strconv.FormatInt(intv/3600, 10) + " hours ago"
}
y1, m1, d1 := now.Date()
diff --git a/quick-start/cql/start.cql b/quick-start/cql/start.cql
index df3ba4d3..382fb9ea 100644
--- a/quick-start/cql/start.cql
+++ b/quick-start/cql/start.cql
@@ -51,7 +51,8 @@ CREATE TABLE IF NOT EXISTS tags (
CREATE TABLE IF NOT EXISTS comment (
id text,
- uid text, -- author user id
+ pid text, -- parent comment id
+ uid text, -- author user id
post_id text, -- related post id
post_type tinyint, -- related post type
@@ -60,11 +61,16 @@ CREATE TABLE IF NOT EXISTS comment (
render text, -- rendered html
publish_date bigint,
+ edit_date bigint,
+
+ status tinyint, -- 0: normal, 1: deleted
PRIMARY KEY (id)
) WITH gc_grace_seconds = 10800;
CREATE CUSTOM INDEX IF NOT EXISTS ON comment (post_id)
USING 'org.apache.cassandra.index.sasi.SASIIndex' ;
+
+
CREATE TABLE IF NOT EXISTS post_counter (
id text, -- post id
@@ -73,4 +79,21 @@ CREATE TABLE IF NOT EXISTS post_counter (
recommands counter,
PRIMARY KEY (id)
-) WITH gc_grace_seconds = 10800;
\ No newline at end of file
+) WITH gc_grace_seconds = 10800;
+
+CREATE TABLE IF NOT EXISTS comment_counter (
+ id text, -- comment id
+ likes counter,
+ PRIMARY KEY (id)
+) WITH gc_grace_seconds = 10800;
+
+-- record whether a user likes a comment
+CREATE TABLE IF NOT EXISTS comment_like (
+ comment_id text, -- comment id
+ uid text, -- user id
+ type tinyint, -- 1: like 2: dislike
+ input_date bigint,
+ PRIMARY KEY (comment_id,uid)
+) WITH gc_grace_seconds = 10800;
+CREATE CUSTOM INDEX IF NOT EXISTS ON comment_like (uid)
+ USING 'org.apache.cassandra.index.sasi.SASIIndex' ;
diff --git a/ui/src/assets/icon/iconfont.css b/ui/src/assets/icon/iconfont.css
new file mode 100644
index 00000000..69f13563
--- /dev/null
+++ b/ui/src/assets/icon/iconfont.css
@@ -0,0 +1,37 @@
+@font-face {font-family: "iconfont";
+ src: url('iconfont.eot?t=1568170482672'); /* IE9 */
+ src: url('iconfont.eot?t=1568170482672#iefix') format('embedded-opentype'), /* IE6-IE8 */
+ url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAPEAAsAAAAACEgAAAN1AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDOAqDQIMIATYCJAMYCw4ABCAFhG0HZBs6BxEVnHnIfh7GtojtKjpzws2LWP1NWda478HzlIe9yZ+kKYRT11ZW9lFAunYSIKBu37nvH9BA+KhzQ/ntkUc7TIGy16YH1Uf/w/1Ml+L57NtcolqjEgc4HtC0Isl2gHgK4tl08D8Bu6EAP5Jkp8pXrF4fG40+SAGqe5dObbATSnRFpMJGYRXM1JFqFia2Mdk4D8z0/l69IW82GJiCvlftjhXaU9qNcQuK4zk0F4exw+kBfh4IkB1oUN0LjR2RwJmd4Kf6a4oC29AbMJQzv1vQ8xhcXvZWGJXBAYBgbOMfzwRloYcHAFAdLhPcGAuBgJsXgQFuPgQK3PwINLgFszABsAZMt4EBwEGKzxCr49cpvwD9gsiYOrDHrbtt6diJJw/a3bzZ8NatRjduNLh+3afHaeDVx1vaLFi6cX1kg/mL1m2IGiZrblYMOn49xVh9owLz10c3Cjx1q5EfteQBpH39G8zfGK38OX469e12u/HhGxVCY+eU7bpjSq97zY7drBhy/HqZLtun/hrVemOjWmXN/6P0kT+nFWOFO/7bJCtLtss2h7+No0d00OC7vdQsOXxYbN5Mjg4GQwNvFpKnFVmSrdbhIaVruq3I3+HSZuXKtUvXpkIz8O8eUGTB+mHDkoTD+x1HObrXVqWdQt4f0bqgSED3ejb2HL8QaPik59muFb5mV5G+NP5Fc6oXzd7VHQDPkSvigHdIBkhpL3SA8cYTf/2qh9Hhcr7/rYKLf7P9NQB3Voz5jB5ej3bEgU/Dasz6hnLtVKByNY6usrTCstAOnyZ0MDWyRQ5iNwA/QwNfzwthHg4lipyI6ZhtIiMw8CERBJsMtMZnBxN/8oOFTSnwI5tK8/0J1xdBaV8g03gCihA7wSCIQyCEuIDW+AdgEuUdWIQkwCt8F75i6TFdlIu4MO6w/oPeKAhTw4Uf+Y4yuyXGOR7hG2PSFhjaPu15w4BxiDHpR47MAkQkD69kP3SO4IhkcONWMR/nrhNFX2o38qfCkwgtGNqB1T+gbUggnNKsyPz+HZIyZxErCqrU31CU6MrBoNU3QL8JQ6OCS7kn+SGNGBOAEBEPvBJ/5DikCY/ibgbasJbqYD+cdUYx0VTYTi/2d7kM8KOfY2UoUVqZylI+XB92uu1HOWWqdvFgj4FTsziuu+olMOUpqSXIMuH408vpBAAAAA==') format('woff2'),
+ url('iconfont.woff?t=1568170482672') format('woff'),
+ url('iconfont.ttf?t=1568170482672') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
+ url('iconfont.svg?t=1568170482672#iconfont') format('svg'); /* iOS 4.1- */
+}
+
+.iconfont {
+ font-family: "iconfont" !important;
+ font-size: 16px;
+ font-style: normal;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+.icon-undo:before {
+ content: "\e633";
+}
+
+.icon-sousuo:before {
+ content: "\e632";
+}
+
+.icon-comments-alt:before {
+ content: "\e618";
+}
+
+.icon-jiantou_shang:before {
+ content: "\e634";
+}
+
+.icon-jiantou_xia:before {
+ content: "\e636";
+}
+
diff --git a/ui/src/assets/icon/iconfont.eot b/ui/src/assets/icon/iconfont.eot
new file mode 100644
index 00000000..35cb862d
Binary files /dev/null and b/ui/src/assets/icon/iconfont.eot differ
diff --git a/ui/src/assets/icon/iconfont.svg b/ui/src/assets/icon/iconfont.svg
new file mode 100644
index 00000000..4e529708
--- /dev/null
+++ b/ui/src/assets/icon/iconfont.svg
@@ -0,0 +1,41 @@
+
+
+
+
diff --git a/ui/src/assets/icon/iconfont.ttf b/ui/src/assets/icon/iconfont.ttf
new file mode 100644
index 00000000..1df09f3e
Binary files /dev/null and b/ui/src/assets/icon/iconfont.ttf differ
diff --git a/ui/src/assets/icon/iconfont.woff b/ui/src/assets/icon/iconfont.woff
new file mode 100644
index 00000000..0748929c
Binary files /dev/null and b/ui/src/assets/icon/iconfont.woff differ
diff --git a/ui/src/assets/icons/downvote.svg b/ui/src/assets/icons/downvote.svg
new file mode 100644
index 00000000..fdc2c69c
--- /dev/null
+++ b/ui/src/assets/icons/downvote.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/assets/icons/upvote.svg b/ui/src/assets/icons/upvote.svg
new file mode 100644
index 00000000..c37c03e9
--- /dev/null
+++ b/ui/src/assets/icons/upvote.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/ui/src/main.js b/ui/src/main.js
index ae053204..52c8c36f 100644
--- a/ui/src/main.js
+++ b/ui/src/main.js
@@ -12,6 +12,8 @@ import "./theme/editor.css";
Vue.use(mavonEditor)
+import './assets/icon/iconfont.css';
+
import App from './App'
Vue.use(ElementUI);
diff --git a/ui/src/router/index.js b/ui/src/router/index.js
index a5f02597..a58b4caa 100644
--- a/ui/src/router/index.js
+++ b/ui/src/router/index.js
@@ -14,7 +14,7 @@ const router = new Router({
component: Nav,
children: [
{ path: '/', meta: {'title':'im.dev'}, component: () => import('@/views/home')},
- { path: '/x/article/new', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')},
+ { path: '/dev/article/new', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')},
{ path: '/:uname/:arID', meta: {'title':'Article - im.dev',},component: () => import('@/views/article/detail')},
{ path: '/:uname/:arID/edit', meta: {'title':'Post - im.dev'},component: () => import('@/views/article/edit')},
]
diff --git a/ui/src/theme/light/style.less b/ui/src/theme/light/style.less
index 2ebbd7a4..64b2e272 100644
--- a/ui/src/theme/light/style.less
+++ b/ui/src/theme/light/style.less
@@ -5,4 +5,16 @@
.post-huge-title {
font-size: 36px;
-}
\ No newline at end of file
+}
+
+.meta-word {
+ font-size:12px;
+ color:rgb(124,124,124);
+}
+
+.bold-meta-word {
+ color:rgb(135, 138, 140);
+ font-weight:700;
+ font-size:12px;
+}
+
diff --git a/ui/src/theme/md_render.css b/ui/src/theme/md_render.css
index 0a7d46c0..e5c9448e 100644
--- a/ui/src/theme/md_render.css
+++ b/ui/src/theme/md_render.css
@@ -1,5 +1,5 @@
.render .content p {
- margin: 0 0 25px
+ margin: 0 0 8px
}
@@ -79,10 +79,10 @@
.render .content h4,
.render .content h5,
.render .content h6 {
- margin: 0 0 15px;
+ margin: 0 0 12px;
font-weight: 700;
color: #2f2f2f;
- line-height: 1.7;
+ line-height: 1.6;
text-rendering: optimizelegibility
}
@@ -129,45 +129,7 @@
-.render .content ol {
- list-style-type: square;
-}
-.render .content ul {
- list-style-type: none;
-}
-
-.render .content ol,
-.render .content ul {
- padding: 0;
- margin-left: 16px;
- margin-bottom: 20px;
- display: block;
-}
-
-.render .content ol {
- margin-left: 36px;
-}
-
-.render .content ol li,
-.render .content ul li {
- margin-bottom: 10px;
- /* line-height: 30px */
-}
-
-.render .content ol li ol,
-.render .content ol li ul,
-.render .content ul li ol,
-.render .content ul li ul {
- margin-top: 15px
-}
-
-.render .content ul li:before {
- font-size: 19.8px;
- padding-top: 4px;
- padding-right: 12px;
- content: '\2022';
-}
.render .content ol .image-package,
.render .content ul .image-package {
@@ -418,3 +380,6 @@
}
+
+
+
diff --git a/ui/src/theme/style.less b/ui/src/theme/style.less
index cfb29833..806877fa 100644
--- a/ui/src/theme/style.less
+++ b/ui/src/theme/style.less
@@ -60,4 +60,8 @@ input::-webkit-input-placeholder {
.background-light-grey {
background-color: #fafafa!important
+}
+
+.el-message.el-message--error.network-error {
+ top: 20px !important;
}
\ No newline at end of file
diff --git a/ui/src/utils/request.js b/ui/src/utils/request.js
index 1e5f0e6b..36416a0c 100644
--- a/ui/src/utils/request.js
+++ b/ui/src/utils/request.js
@@ -31,6 +31,14 @@ service.interceptors.response.use(
},
error => {
var response = error.response
+ if (response == undefined) {
+ Message.error({
+ message: error.message,
+ duration: 3000,
+ customClass: 'network-error'
+ })
+ return Promise.reject(error)
+ }
if (response.data.err_code == 1001) {
store.dispatch("setNeedSignin", 1)
store.dispatch("ClearUserInfo")
@@ -48,10 +56,7 @@ service.interceptors.response.use(
return Promise.reject(response.data.message+' : '+ response.data.err_code)
}
- Message.error({
- content: error.message,
- duration: 3
- })
+ Message.error(error.message)
return Promise.reject(error)
})
diff --git a/ui/src/views/article/detail.vue b/ui/src/views/article/detail.vue
index 0b8d954b..6d406926 100644
--- a/ui/src/views/article/detail.vue
+++ b/ui/src/views/article/detail.vue
@@ -50,8 +50,8 @@
{{arDetail.title}}
@@ -62,10 +62,10 @@
-
+
@@ -111,7 +111,6 @@ export default {
article_id: this.arID
}
}).then(res0 => {
- console.log(222)
request({
url: "/web/user/get",
method: "GET",
@@ -137,7 +136,6 @@ export default {
.square {
margin-top: 3px;
line-height: 36px;
- border: 1px solid white;
text-align: center;
z-index: 100;
font-size: 25px;
diff --git a/ui/src/views/article/edit.vue b/ui/src/views/article/edit.vue
index 4a71450c..45d69d23 100644
--- a/ui/src/views/article/edit.vue
+++ b/ui/src/views/article/edit.vue
@@ -33,7 +33,7 @@
-
+
diff --git a/ui/src/views/components/discuss.vue b/ui/src/views/components/discuss.vue
index 78140d1b..fd8fba3b 100644
--- a/ui/src/views/components/discuss.vue
+++ b/ui/src/views/components/discuss.vue
@@ -1,27 +1,73 @@