Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
636ec40
A UI to allow you to select a person to mention
mattwoberts Nov 7, 2024
259645b
Upgrade to the latest version of react.
mattwoberts Nov 10, 2024
77b53a9
Upgraded some react deps, added slate
mattwoberts Nov 10, 2024
7812810
React upgrade is a little more picky about types.
mattwoberts Nov 15, 2024
4785ea1
A working comment input, but it needs refactoring to make it sharable.
mattwoberts Nov 15, 2024
21c43ed
Migrating existing functionality to use slate editor
mattwoberts Nov 17, 2024
a18edc7
Mentions work but much refatoring needed.
mattwoberts Nov 24, 2024
d975cbc
Removed some formatting stuff we don't need yet
mattwoberts Nov 25, 2024
c174271
Adding a comment with mentinos
mattwoberts Nov 28, 2024
a7f70f8
Adding slate js lib
mattwoberts Nov 28, 2024
102cd07
Notifications settings for mentions
mattwoberts Dec 3, 2024
39c0ed2
Editing a comment
mattwoberts Dec 3, 2024
3b17fd7
Formatting the slate editor
mattwoberts Dec 3, 2024
74cc697
Renaming files and tidying up a bit
mattwoberts Dec 3, 2024
881a1b7
Webhooks delete wasn't updating, plus styling tweaks for the HTTP Hea…
mattwoberts Dec 4, 2024
bce520a
Added a function to remove mention encoding from comments. Plumbed in…
mattwoberts Dec 5, 2024
f40c9ca
console.log
mattwoberts Dec 5, 2024
f941a56
Remove encoding from mentions in comment retrieval in API
mattwoberts Dec 5, 2024
6b7b194
Mention notifications.
mattwoberts Dec 9, 2024
33fbd8e
Moving tests and code around
mattwoberts Dec 16, 2024
f0abcdf
Updated render for react 18+
mattwoberts Jan 6, 2025
f2b9d1d
Updated lingui to latest version, and changes to support this.
mattwoberts Jan 7, 2025
37bb47d
Changes for react 18
mattwoberts Jan 7, 2025
6562a7a
Webpack changes
mattwoberts Jan 7, 2025
0d2a268
Finished the upgrade to lingui 5
mattwoberts Jan 9, 2025
1136016
Fixed some of the test
mattwoberts Jan 9, 2025
0042db2
Fix the SSR stuff with react 18
mattwoberts Jan 10, 2025
31ca880
Update babel
mattwoberts Jan 10, 2025
db537c6
Merge branch 'main' into mentions
mattwoberts Jan 10, 2025
367203f
Updated lock file after merge
mattwoberts Jan 10, 2025
09bee2c
New languages in new lingui config
mattwoberts Jan 11, 2025
1377cf2
Lintint
mattwoberts Jan 11, 2025
d7bb171
Prettier formatting
mattwoberts Jan 11, 2025
510937b
Pretter formatting]
mattwoberts Jan 11, 2025
bdf9d80
More linting
mattwoberts Jan 11, 2025
e3b02dd
Merge branch 'main' of https://github.com/getfider/fider into mentions
mattwoberts Feb 6, 2025
ef93be4
Few bits needed sorting after the merge
mattwoberts Feb 6, 2025
3565c96
Don't render mentions as links
mattwoberts Feb 6, 2025
86262de
Code formatting in markdown
mattwoberts Feb 6, 2025
e967529
Smaller lozenge on listing
mattwoberts Feb 7, 2025
e19088e
Tidying up the querystrting stuff.
mattwoberts Feb 7, 2025
541c481
User logged out bugs
mattwoberts Feb 10, 2025
4864cd0
Only get taggable users when we try to use the mention feature
mattwoberts Feb 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .babelrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"presets": ["@babel/env", "@babel/react", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-proposal-class-properties", "macros"]
"plugins": [
"@babel/plugin-proposal-class-properties",
["macros", {
"lingui": {
"version": 5
}
}],
"@lingui/babel-plugin-lingui-macro"
]
}
1 change: 1 addition & 0 deletions app/cmd/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ func routes(r *web.Engine) *web.Engine {
membersApi.Post("/api/v1/posts/:number/comments", apiv1.PostComment())
membersApi.Put("/api/v1/posts/:number/comments/:id", apiv1.UpdateComment())
membersApi.Delete("/api/v1/posts/:number/comments/:id", apiv1.DeleteComment())
membersApi.Get("/api/v1/taggable-users", apiv1.ListTaggableUsers())
membersApi.Post("/api/v1/posts/:number/votes", apiv1.AddVote())
membersApi.Delete("/api/v1/posts/:number/votes", apiv1.RemoveVote())
membersApi.Post("/api/v1/posts/:number/votes/toggle", apiv1.ToggleVote())
Expand Down
36 changes: 32 additions & 4 deletions app/handlers/apiv1/post.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package apiv1

import (
"fmt"

"github.com/getfider/fider/app/actions"
"github.com/getfider/fider/app/metrics"
"github.com/getfider/fider/app/models/cmd"
Expand All @@ -9,6 +11,7 @@ import (
"github.com/getfider/fider/app/models/query"
"github.com/getfider/fider/app/pkg/bus"
"github.com/getfider/fider/app/pkg/env"
"github.com/getfider/fider/app/pkg/markdown"
"github.com/getfider/fider/app/pkg/web"
"github.com/getfider/fider/app/tasks"
)
Expand Down Expand Up @@ -212,6 +215,11 @@ func ListComments() web.HandlerFunc {
return c.Failure(err)
}

// the content of the comment needs to be sanitized before it is returned
for _, comment := range getComments.Result {
comment.Content = markdown.StripMentionMetaData(comment.Content)
}

return c.Ok(getComments.Result)
}
}
Expand All @@ -229,6 +237,8 @@ func GetComment() web.HandlerFunc {
return c.Failure(err)
}

commentByID.Result.Content = markdown.StripMentionMetaData(commentByID.Result.Content)

return c.Ok(commentByID.Result)
}
}
Expand Down Expand Up @@ -279,13 +289,18 @@ func PostComment() web.HandlerFunc {
}

addNewComment := &cmd.AddNewComment{
Post: getPost.Result,
Content: action.Content,
Post: getPost.Result,
Content: entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
}),
}
if err := bus.Dispatch(c, addNewComment); err != nil {
return c.Failure(err)
}

// For processing, restore the original content
addNewComment.Result.Content = action.Content

if err := bus.Dispatch(c, &cmd.SetAttachments{
Post: getPost.Result,
Comment: addNewComment.Result,
Expand All @@ -294,7 +309,7 @@ func PostComment() web.HandlerFunc {
return c.Failure(err)
}

c.Enqueue(tasks.NotifyAboutNewComment(getPost.Result, action.Content))
c.Enqueue(tasks.NotifyAboutNewComment(addNewComment.Result, getPost.Result))

metrics.TotalComments.Inc()
return c.Ok(web.Map{
Expand All @@ -311,14 +326,23 @@ func UpdateComment() web.HandlerFunc {
return c.HandleValidation(result)
}

getPost := &query.GetPostByID{PostID: action.Post.ID}
if err := bus.Dispatch(c, getPost); err != nil {
return c.Failure(err)
}

contentToSave := entity.CommentString(action.Content).FormatMentionJson(func(mention entity.Mention) string {
return fmt.Sprintf(`{"id":%d,"name":"%s"}`, mention.ID, mention.Name)
})

err := bus.Dispatch(c,
&cmd.UploadImages{
Images: action.Attachments,
Folder: "attachments",
},
&cmd.UpdateComment{
CommentID: action.ID,
Content: action.Content,
Content: contentToSave,
},
&cmd.SetAttachments{
Post: action.Post,
Expand All @@ -330,6 +354,10 @@ func UpdateComment() web.HandlerFunc {
return c.Failure(err)
}

// Update the content

c.Enqueue(tasks.NotifyAboutUpdatedComment(action.Content, getPost.Result))

return c.Ok(web.Map{})
}
}
Expand Down
35 changes: 35 additions & 0 deletions app/handlers/apiv1/post_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -723,6 +723,36 @@ func TestPostCommentHandler(t *testing.T) {
Expect(newComment.Content).Equals("This is a comment!")
}

func TestPostCommentHandlerMentions(t *testing.T) {
RegisterT(t)

post := &entity.Post{ID: 1, Number: 1, Title: "The Post #1", Description: "The Description #1"}
bus.AddHandler(func(ctx context.Context, q *query.GetPostByNumber) error {
q.Result = post
return nil
})

var newComment *cmd.AddNewComment
bus.AddHandler(func(ctx context.Context, c *cmd.AddNewComment) error {
newComment = c
c.Result = &entity.Comment{ID: 1, Content: c.Content}
return nil
})

bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil })
bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil })

code, _ := mock.NewServer().
OnTenant(mock.DemoTenant).
AsUser(mock.JonSnow).
AddParam("number", post.Number).
ExecutePost(apiv1.PostComment(), `{ "content": "Hello @{\"id\":1,\"name\":\"Jon Snow\",\"isNew\":true}!" }`)

Expect(code).Equals(http.StatusOK)
Expect(newComment.Post).Equals(post)
Expect(newComment.Content).Equals("Hello @{\"id\":1,\"name\":\"Jon Snow\"}!")
}

func TestPostCommentHandler_WithoutContent(t *testing.T) {
RegisterT(t)

Expand Down Expand Up @@ -758,6 +788,11 @@ func TestUpdateCommentHandler_Authorized(t *testing.T) {
return nil
})

bus.AddHandler(func(ctx context.Context, q *query.GetPostByID) error {
q.Result = post
return nil
})

bus.AddHandler(func(ctx context.Context, q *query.GetAttachments) error { return nil })
bus.AddHandler(func(ctx context.Context, c *cmd.SetAttachments) error { return nil })
bus.AddHandler(func(ctx context.Context, c *cmd.UploadImages) error { return nil })
Expand Down
10 changes: 10 additions & 0 deletions app/handlers/apiv1/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@ func ListUsers() web.HandlerFunc {
}
}

func ListTaggableUsers() web.HandlerFunc {
return func(c *web.Context) error {
allUsers := &query.GetAllUsersNames{}
if err := bus.Dispatch(c, allUsers); err != nil {
return c.Failure(err)
}
return c.Ok(allUsers.Result)
}
}

// CreateUser is used to create new users
func CreateUser() web.HandlerFunc {
return func(c *web.Context) error {
Expand Down
6 changes: 6 additions & 0 deletions app/models/dto/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package dto

type UserNames struct {
ID int `json:"id"`
Name string `json:"name"`
}
10 changes: 9 additions & 1 deletion app/models/entity/comment.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package entity

import "time"
import (
"time"
)

type ReactionCounts struct {
Emoji string `json:"emoji"`
Expand All @@ -18,4 +20,10 @@ type Comment struct {
EditedAt *time.Time `json:"editedAt,omitempty"`
EditedBy *User `json:"editedBy,omitempty"`
ReactionCounts []ReactionCounts `json:"reactionCounts,omitempty"`
Mentions []Mention `json:"_"`
}

func (c *Comment) ParseMentions() {
mentionString := CommentString(c.Content)
c.Mentions = mentionString.ParseMentions()
}
69 changes: 69 additions & 0 deletions app/models/entity/mention.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package entity

import (
"encoding/json"
"regexp"
"strings"
)

type Mention struct {
ID int `json:"id"`
Name string `json:"name"`
IsNew bool `json:"isNew"`
}

type CommentString string

const mentionRegex = `@{([^{}]+)}`

func (commentString CommentString) ParseMentions() []Mention {
r, _ := regexp.Compile(mentionRegex)

// Remove escaped quotes from the input string
input := strings.ReplaceAll(string(commentString), `\"`, `"`)

matches := r.FindAllString(input, -1)

mentions := []Mention{}

for _, match := range matches {

jsonMention := match[1:]

var mention Mention
err := json.Unmarshal([]byte(jsonMention), &mention)
if err == nil {
if mention.ID > 0 && mention.Name != "" {
mentions = append(mentions, mention)
}
}
}

return mentions
}

func (mentionString CommentString) FormatMentionJson(jsonOperator func(Mention) string) string {

r, _ := regexp.Compile(mentionRegex)

// Remove escaped quotes from the input string
input := strings.ReplaceAll(string(mentionString), `\"`, `"`)

return r.ReplaceAllStringFunc(input, func(match string) string {
jsonMention := match[1:]

var mention Mention

err := json.Unmarshal([]byte(jsonMention), &mention)
if err != nil {
return match
}

if mention.ID == 0 || mention.Name == "" {
return match
}

return "@" + jsonOperator(mention)
})

}
92 changes: 92 additions & 0 deletions app/models/entity/mention_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package entity_test

import (
"testing"

"github.com/getfider/fider/app/models/entity"

. "github.com/getfider/fider/app/pkg/assert"
)

func TestComment_ParseMentions(t *testing.T) {
RegisterT(t)
tests := []struct {
name string
content string
expected []entity.Mention
}{
{
name: "no mentions",
content: "This is a regular comment \\\" just here",
expected: []entity.Mention{},
},
{
name: "Simple mention",
content: `Hello there @{"id":1,"name":"John Doe","isNew":false} how are you`,
expected: []entity.Mention{{ID: 1, Name: "John Doe", IsNew: false}},
},
{
name: "Simple mention 2",
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} how are you`,
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}},
},
{
name: "Multiple mentions",
content: `Hello there @{"id":2,"name":"John Doe Smith","isNew":true} and @{"id":1,"name":"John Doe","isNew":false} how are you`,
expected: []entity.Mention{{ID: 2, Name: "John Doe Smith", IsNew: true}, {ID: 1, Name: "John Doe", IsNew: false}},
},
{
name: "Some odd JSON",
content: `Hello there @{"id":2,name:"John Doe Smith","isNew":true} how are you`,
expected: []entity.Mention{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
comment := &entity.Comment{Content: tt.content}
comment.ParseMentions()
Expect(comment.Mentions).Equals(tt.expected)
})
}
}

func TestStripMentionMetaData(t *testing.T) {
RegisterT(t)

for input, expected := range map[string]string{
`@{\"id\":1,\"name\":\"John Doe quoted\"}`: "@John Doe quoted",
`@{"id":1,"name":"John Doe"}`: "@John Doe",
`@{"id":1,"name":"JohnDoe"}`: "@JohnDoe",
`@{\"id\":1,\"name\":\"JohnDoe quoted\"}`: "@JohnDoe quoted",
`@{"id":1,"name":"John Smith Doe"}`: "@John Smith Doe",
`@{\"id\":1,\"name\":\"John Smith Doe quoted\"}`: "@John Smith Doe quoted",
"Hello there how are you": "Hello there how are you",
`Hello there @{"id":1,"name":"John Doe"}`: "Hello there @John Doe",
`Hello there @{"id":1,"name":"John Doe quoted"}`: "Hello there @John Doe quoted",
`Hello both @{"id":1,"name":"John Doe"} and @{"id":2,"name":"John Smith"}`: "Hello both @John Doe and @John Smith",
} {
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
return mention.Name
})
Expect(output).Equals(expected)
}
}

func TestStripMentionMetaDataDoesntBreakUserInput(t *testing.T) {
RegisterT(t)

for input, expected := range map[string]string{
`There is nothing here`: "There is nothing here",
`There is nothing here {ok}`: "There is nothing here {ok}",
`This is a message for {{matt}}`: "This is a message for {{matt}}",
`This is a message for {{id:1,wiggles:true}}`: "This is a message for {{id:1,wiggles:true}}",
`Although uncommon, someone could enter @{something} like this`: "Although uncommon, someone could enter @{something} like this",
`Or @{"id":100,"wiggles":"yes"} something like this`: `Or @{"id":100,"wiggles":"yes"} something like this`,
} {
output := entity.CommentString(input).FormatMentionJson(func(mention entity.Mention) string {
return mention.Name
})
Expect(output).Equals(expected)
}
}
Loading
Loading