mirror of
https://github.com/gotify/server.git
synced 2024-01-28 15:20:56 +03:00
WIP
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/gotify/location"
|
||||
"github.com/gotify/server/auth"
|
||||
"github.com/gotify/server/model"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// The MessageDatabase interface for encapsulating database access.
|
||||
@@ -18,12 +19,15 @@ type MessageDatabase interface {
|
||||
GetMessagesByApplicationSince(appID uint, limit int, since uint) []*model.Message
|
||||
GetApplicationByID(id uint) *model.Application
|
||||
GetMessagesByUserSince(userID uint, limit int, since uint) []*model.Message
|
||||
GetUnreadMessagesByUserSince(userID uint, limit int, since uint) []*model.Message
|
||||
DeleteMessageByID(id uint) error
|
||||
GetMessageByID(id uint) *model.Message
|
||||
DeleteMessagesByUser(userID uint) error
|
||||
DeleteMessagesByApplication(applicationID uint) error
|
||||
CreateMessage(message *model.Message) error
|
||||
GetApplicationByToken(token string) *model.Application
|
||||
GetApplicationsByUser(userID uint) []*model.Application
|
||||
SetMessagesRead(ids []uint) error
|
||||
}
|
||||
|
||||
// Notifier notifies when a new message was created.
|
||||
@@ -42,6 +46,10 @@ type pagingParams struct {
|
||||
Since uint `form:"since" binding:"min=0"`
|
||||
}
|
||||
|
||||
type markAsReadParams struct {
|
||||
Ids []uint `form:"id"`
|
||||
}
|
||||
|
||||
// GetMessages returns all messages from a user.
|
||||
func (a *MessageAPI) GetMessages(ctx *gin.Context) {
|
||||
userID := auth.GetUserID(ctx)
|
||||
@@ -52,6 +60,47 @@ func (a *MessageAPI) GetMessages(ctx *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// GetUnreadMessages returns all unread messages from a user.
|
||||
func (a *MessageAPI) GetUnreadMessages(ctx *gin.Context) {
|
||||
userID := auth.GetUserID(ctx)
|
||||
withPaging(ctx, func(params *pagingParams) {
|
||||
// the +1 is used to check if there are more messages and will be removed on buildWithPaging
|
||||
messages := a.DB.GetUnreadMessagesByUserSince(userID, params.Limit+1, params.Since)
|
||||
ctx.JSON(200, buildWithPaging(ctx, params, messages))
|
||||
})
|
||||
}
|
||||
|
||||
// MarkMessagesAsRead marks messages as read.
|
||||
func (a *MessageAPI) MarkMessagesAsRead(ctx *gin.Context) {
|
||||
params := &markAsReadParams{}
|
||||
if err := ctx.MustBindWith(params, binding.Query); err == nil {
|
||||
ids := params.Ids
|
||||
appIds := a.userAppIDs(auth.GetUserID(ctx))
|
||||
for _, e := range ids {
|
||||
|
||||
message := a.DB.GetMessageByID(e)
|
||||
if message == nil {
|
||||
ctx.AbortWithError(404, errors.New(fmt.Sprintf("message with id %d does not exist", e)))
|
||||
return
|
||||
} else if _, ok := appIds[message.ApplicationID]; !ok {
|
||||
ctx.AbortWithError(403, errors.New(fmt.Sprintf("you have no permission to change message with id %d", e)))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
a.DB.SetMessagesRead(ids)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *MessageAPI) userAppIDs(userID uint) map[uint]struct{} {
|
||||
apps := a.DB.GetApplicationsByUser(userID)
|
||||
appIds := make(map[uint]struct{})
|
||||
for _, app := range apps {
|
||||
appIds[app.ID] = struct{}{}
|
||||
}
|
||||
return appIds
|
||||
}
|
||||
|
||||
func buildWithPaging(ctx *gin.Context, paging *pagingParams, messages []*model.Message) *model.PagedMessages {
|
||||
next := ""
|
||||
since := uint(0)
|
||||
|
||||
@@ -59,7 +59,7 @@ func (s *MessageSuite) Test_ensureCorrectJsonRepresentation() {
|
||||
Messages: []*model.Message{{ID: 55, ApplicationID: 2, Message: "hi", Title: "hi", Date: t, Priority: 4}},
|
||||
}
|
||||
test.JSONEquals(s.T(), actual, `{"paging": {"limit":5, "since": 122, "size": 5, "next": "http://example.com/message?limit=5&since=122"},
|
||||
"messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z"}]}`)
|
||||
"messages": [{"id":55,"appid":2,"message":"hi","title":"hi","priority":4,"date":"2017-01-02T00:00:00Z", "read": false}]}`)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages() {
|
||||
@@ -78,6 +78,23 @@ func (s *MessageSuite) Test_GetMessages() {
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetUnreadMessages() {
|
||||
user := s.db.User(5)
|
||||
first := user.App(1).NewMessage(1)
|
||||
second := user.App(2).NewMessage(2)
|
||||
user.App(3).NewMessage(3);
|
||||
s.db.SetMessagesRead([]uint{3});
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.a.GetUnreadMessages(s.ctx)
|
||||
|
||||
expected := &model.PagedMessages{
|
||||
Paging: model.Paging{Limit: 100, Size: 2, Next: ""},
|
||||
Messages: []*model.Message{&second, &first,},
|
||||
}
|
||||
|
||||
test.BodyEquals(s.T(), expected, s.recorder)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_WithLimit_ReturnsNext() {
|
||||
user := s.db.User(5)
|
||||
app1 := user.App(1)
|
||||
@@ -134,6 +151,15 @@ func (s *MessageSuite) Test_GetMessages_BadRequestOnInvalidLimit() {
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetUnreadMessages_BadRequestOnInvalidLimit() {
|
||||
s.db.User(5)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.withURL("http", "example.com", "/messages", "limit=555")
|
||||
s.a.GetUnreadMessages(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessages_BadRequestOnInvalidLimit_Negative() {
|
||||
s.db.User(5)
|
||||
test.WithUser(s.ctx, 5)
|
||||
@@ -143,6 +169,15 @@ func (s *MessageSuite) Test_GetMessages_BadRequestOnInvalidLimit_Negative() {
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetUnreadMessages_BadRequestOnInvalidLimit_Negative() {
|
||||
s.db.User(5)
|
||||
test.WithUser(s.ctx, 5)
|
||||
s.withURL("http", "example.com", "/messages", "limit=-5")
|
||||
s.a.GetUnreadMessages(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 400, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_GetMessagesWithToken_InvalidLimit_BadRequest() {
|
||||
s.db.User(4).App(2).NewMessage(1)
|
||||
|
||||
@@ -437,6 +472,102 @@ func (s *MessageSuite) Test_CreateMessage_onFormData() {
|
||||
assert.True(s.T(), s.notified)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead() {
|
||||
user := s.db.User(2)
|
||||
user.App(1).Message(1).Message(2)
|
||||
user.App(2).Message(3).Message(4)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=3")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
assert.False(s.T(), s.db.GetMessageByID(1).Read)
|
||||
assert.False(s.T(), s.db.GetMessageByID(2).Read)
|
||||
assert.True(s.T(), s.db.GetMessageByID(3).Read)
|
||||
assert.False(s.T(), s.db.GetMessageByID(4).Read)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead_multiple() {
|
||||
user := s.db.User(2)
|
||||
user.App(1).Message(1).Message(2)
|
||||
user.App(2).Message(3).Message(4)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=3&id=2")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 200, s.recorder.Code)
|
||||
assert.False(s.T(), s.db.GetMessageByID(1).Read)
|
||||
assert.True(s.T(), s.db.GetMessageByID(2).Read)
|
||||
assert.True(s.T(), s.db.GetMessageByID(3).Read)
|
||||
assert.False(s.T(), s.db.GetMessageByID(4).Read)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead_wrongUser() {
|
||||
one := s.db.User(2)
|
||||
one.App(1).Message(1).Message(2)
|
||||
one.App(2).Message(3).Message(4)
|
||||
|
||||
two := s.db.User(3)
|
||||
two.App(3).Message(5)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=5")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 403, s.recorder.Code)
|
||||
assert.False(s.T(), s.db.GetMessageByID(5).Read)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead_wrongUser_onlyOneIdUnauthorized() {
|
||||
one := s.db.User(2)
|
||||
one.App(1).Message(1).Message(2)
|
||||
one.App(2).Message(3).Message(4)
|
||||
|
||||
two := s.db.User(3)
|
||||
two.App(3).Message(5)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=5&id=4")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 403, s.recorder.Code)
|
||||
assert.False(s.T(), s.db.GetMessageByID(5).Read)
|
||||
assert.False(s.T(), s.db.GetMessageByID(4).Read)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead_unknownId() {
|
||||
one := s.db.User(2)
|
||||
one.App(1).Message(1).Message(2)
|
||||
one.App(2).Message(3).Message(4)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=5")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) Test_MarkAsRead_unknownId_oneExistingId() {
|
||||
one := s.db.User(2)
|
||||
one.App(1).Message(1).Message(2)
|
||||
one.App(2).Message(3).Message(4)
|
||||
|
||||
test.WithUser(s.ctx, 2)
|
||||
s.withURL("http", "example.com", "/message/read", "id=5&id=4")
|
||||
|
||||
s.a.MarkMessagesAsRead(s.ctx)
|
||||
|
||||
assert.Equal(s.T(), 404, s.recorder.Code)
|
||||
assert.False(s.T(), s.db.GetMessageByID(4).Read)
|
||||
}
|
||||
|
||||
func (s *MessageSuite) withURL(scheme, host, path, query string) {
|
||||
s.ctx.Request.URL = &url.URL{Path: path, RawQuery: query}
|
||||
s.ctx.Set("location", &url.URL{Scheme: scheme, Host: host})
|
||||
|
||||
112
api/profile.out
Normal file
112
api/profile.out
Normal file
@@ -0,0 +1,112 @@
|
||||
mode: atomic
|
||||
github.com\gotify\server\api\internalutil.go:10.61,11.71 1 37
|
||||
github.com\gotify\server\api\internalutil.go:11.71,13.3 1 33
|
||||
github.com\gotify\server\api\internalutil.go:13.3,15.3 1 4
|
||||
github.com\gotify\server\api\message.go:54.52,56.45 2 5
|
||||
github.com\gotify\server\api\message.go:56.45,60.3 2 3
|
||||
github.com\gotify\server\api\message.go:64.58,66.45 2 3
|
||||
github.com\gotify\server\api\message.go:66.45,70.3 2 1
|
||||
github.com\gotify\server\api\message.go:74.51,76.64 2 6
|
||||
github.com\gotify\server\api\message.go:76.64,79.25 3 6
|
||||
github.com\gotify\server\api\message.go:91.3,91.28 1 2
|
||||
github.com\gotify\server\api\message.go:79.25,82.22 2 7
|
||||
github.com\gotify\server\api\message.go:82.22,85.5 2 2
|
||||
github.com\gotify\server\api\message.go:85.5,85.58 1 5
|
||||
github.com\gotify\server\api\message.go:85.58,88.5 2 2
|
||||
github.com\gotify\server\api\message.go:95.64,98.27 3 6
|
||||
github.com\gotify\server\api\message.go:101.2,101.15 1 6
|
||||
github.com\gotify\server\api\message.go:98.27,100.3 1 12
|
||||
github.com\gotify\server\api\message.go:104.110,108.34 4 7
|
||||
github.com\gotify\server\api\message.go:119.2,122.3 1 7
|
||||
github.com\gotify\server\api\message.go:108.34,118.3 9 4
|
||||
github.com\gotify\server\api\message.go:125.71,127.64 2 13
|
||||
github.com\gotify\server\api\message.go:127.64,129.3 1 8
|
||||
github.com\gotify\server\api\message.go:133.67,134.34 1 5
|
||||
github.com\gotify\server\api\message.go:134.34,135.46 1 5
|
||||
github.com\gotify\server\api\message.go:135.46,136.91 1 4
|
||||
github.com\gotify\server\api\message.go:136.91,140.5 2 3
|
||||
github.com\gotify\server\api\message.go:140.5,142.5 1 1
|
||||
github.com\gotify\server\api\message.go:148.55,151.2 2 1
|
||||
github.com\gotify\server\api\message.go:154.69,155.34 1 3
|
||||
github.com\gotify\server\api\message.go:155.34,156.114 1 3
|
||||
github.com\gotify\server\api\message.go:156.114,158.4 1 1
|
||||
github.com\gotify\server\api\message.go:158.4,160.4 1 2
|
||||
github.com\gotify\server\api\message.go:165.54,166.34 1 4
|
||||
github.com\gotify\server\api\message.go:166.34,167.125 1 3
|
||||
github.com\gotify\server\api\message.go:167.125,169.4 1 1
|
||||
github.com\gotify\server\api\message.go:169.4,171.4 1 2
|
||||
github.com\gotify\server\api\message.go:176.54,178.43 2 7
|
||||
github.com\gotify\server\api\message.go:178.43,184.3 5 4
|
||||
github.com\gotify\server\api\token.go:43.56,45.39 2 5
|
||||
github.com\gotify\server\api\token.go:45.39,50.3 4 4
|
||||
github.com\gotify\server\api\token.go:54.51,56.42 2 4
|
||||
github.com\gotify\server\api\token.go:56.42,61.3 4 3
|
||||
github.com\gotify\server\api\token.go:65.54,68.27 3 2
|
||||
github.com\gotify\server\api\token.go:71.2,71.21 1 2
|
||||
github.com\gotify\server\api\token.go:68.27,70.3 1 4
|
||||
github.com\gotify\server\api\token.go:75.49,79.2 3 1
|
||||
github.com\gotify\server\api\token.go:82.56,83.34 1 4
|
||||
github.com\gotify\server\api\token.go:83.34,84.90 1 4
|
||||
github.com\gotify\server\api\token.go:84.90,86.23 2 2
|
||||
github.com\gotify\server\api\token.go:86.23,88.5 1 1
|
||||
github.com\gotify\server\api\token.go:89.4,91.4 1 2
|
||||
github.com\gotify\server\api\token.go:96.51,97.34 1 3
|
||||
github.com\gotify\server\api\token.go:97.34,98.94 1 3
|
||||
github.com\gotify\server\api\token.go:98.94,101.4 2 1
|
||||
github.com\gotify\server\api\token.go:101.4,103.4 1 2
|
||||
github.com\gotify\server\api\token.go:108.61,109.34 1 8
|
||||
github.com\gotify\server\api\token.go:109.34,110.90 1 8
|
||||
github.com\gotify\server\api\token.go:110.90,112.34 2 7
|
||||
github.com\gotify\server\api\token.go:119.4,122.31 4 5
|
||||
github.com\gotify\server\api\token.go:127.4,130.39 3 4
|
||||
github.com\gotify\server\api\token.go:134.4,135.18 2 4
|
||||
github.com\gotify\server\api\token.go:140.4,140.23 1 3
|
||||
github.com\gotify\server\api\token.go:144.4,146.44 3 3
|
||||
github.com\gotify\server\api\token.go:112.34,115.5 2 1
|
||||
github.com\gotify\server\api\token.go:115.5,115.25 1 6
|
||||
github.com\gotify\server\api\token.go:115.25,118.5 2 1
|
||||
github.com\gotify\server\api\token.go:122.31,125.5 2 1
|
||||
github.com\gotify\server\api\token.go:130.39,132.5 1 1
|
||||
github.com\gotify\server\api\token.go:135.18,138.5 2 1
|
||||
github.com\gotify\server\api\token.go:140.23,142.5 1 2
|
||||
github.com\gotify\server\api\token.go:147.4,149.4 1 1
|
||||
github.com\gotify\server\api\token.go:153.30,154.49 1 5
|
||||
github.com\gotify\server\api\token.go:157.2,157.13 1 1
|
||||
github.com\gotify\server\api\token.go:154.49,156.3 1 4
|
||||
github.com\gotify\server\api\token.go:160.83,163.21 2 11
|
||||
github.com\gotify\server\api\token.go:168.2,169.12 2 11
|
||||
github.com\gotify\server\api\token.go:163.21,165.3 1 7
|
||||
github.com\gotify\server\api\token.go:165.3,167.3 1 4
|
||||
github.com\gotify\server\api\token.go:172.57,174.2 1 5
|
||||
github.com\gotify\server\api\token.go:176.52,178.2 1 4
|
||||
github.com\gotify\server\api\token.go:180.104,181.6 1 7
|
||||
github.com\gotify\server\api\token.go:181.6,183.26 2 9
|
||||
github.com\gotify\server\api\token.go:183.26,185.4 1 7
|
||||
github.com\gotify\server\api\user.go:30.46,34.29 3 1
|
||||
github.com\gotify\server\api\user.go:38.2,38.21 1 1
|
||||
github.com\gotify\server\api\user.go:34.29,36.3 1 2
|
||||
github.com\gotify\server\api\user.go:42.52,45.2 2 1
|
||||
github.com\gotify\server\api\user.go:48.48,50.40 2 4
|
||||
github.com\gotify\server\api\user.go:50.40,52.47 2 2
|
||||
github.com\gotify\server\api\user.go:52.47,55.4 2 1
|
||||
github.com\gotify\server\api\user.go:55.4,57.4 1 1
|
||||
github.com\gotify\server\api\user.go:62.49,63.34 1 3
|
||||
github.com\gotify\server\api\user.go:63.34,64.54 1 2
|
||||
github.com\gotify\server\api\user.go:64.54,66.4 1 1
|
||||
github.com\gotify\server\api\user.go:66.4,68.4 1 1
|
||||
github.com\gotify\server\api\user.go:73.52,74.34 1 3
|
||||
github.com\gotify\server\api\user.go:74.34,75.48 1 2
|
||||
github.com\gotify\server\api\user.go:75.48,78.4 2 1
|
||||
github.com\gotify\server\api\user.go:78.4,80.4 1 1
|
||||
github.com\gotify\server\api\user.go:85.52,87.38 2 2
|
||||
github.com\gotify\server\api\user.go:87.38,91.3 3 1
|
||||
github.com\gotify\server\api\user.go:95.52,96.34 1 4
|
||||
github.com\gotify\server\api\user.go:96.34,98.41 2 3
|
||||
github.com\gotify\server\api\user.go:98.41,99.55 1 3
|
||||
github.com\gotify\server\api\user.go:99.55,104.5 4 2
|
||||
github.com\gotify\server\api\user.go:104.5,106.5 1 1
|
||||
github.com\gotify\server\api\user.go:111.91,116.25 2 4
|
||||
github.com\gotify\server\api\user.go:121.2,121.13 1 4
|
||||
github.com\gotify\server\api\user.go:116.25,118.3 1 3
|
||||
github.com\gotify\server\api\user.go:118.3,120.3 1 1
|
||||
github.com\gotify\server\api\user.go:124.59,130.2 1 7
|
||||
2
app.go
2
app.go
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
var (
|
||||
// Version the version of Gotify.
|
||||
Version = "unknown"
|
||||
Version = "1.1.7"
|
||||
// Commit the git commit hash of this version.
|
||||
Commit = "unknown"
|
||||
// BuildDate the date on which this binary was build.
|
||||
|
||||
@@ -40,6 +40,24 @@ func (d *GormDatabase) GetMessagesByUserSince(userID uint, limit int, since uint
|
||||
return messages
|
||||
}
|
||||
|
||||
// GetUnreadMessagesByUserSince returns limited messages from a user.
|
||||
// If since is 0 it will be ignored.
|
||||
func (d *GormDatabase) GetUnreadMessagesByUserSince(userID uint, limit int, since uint) []*model.Message {
|
||||
var messages []*model.Message
|
||||
db := d.DB.Joins("JOIN applications ON applications.user_id = ?", userID).
|
||||
Where("messages.application_id = applications.id").Where("messages.read = ?", false).Order("id desc").Limit(limit)
|
||||
if since != 0 {
|
||||
db = db.Where("messages.id < ?", since)
|
||||
}
|
||||
db.Find(&messages)
|
||||
return messages
|
||||
}
|
||||
|
||||
// SetMessagesRead sets the read flag on messages to true
|
||||
func (d *GormDatabase) SetMessagesRead(ids []uint) error {
|
||||
return d.DB.Model(&model.Message{}).Where("id in (?)", ids).Update("read", true).Error
|
||||
}
|
||||
|
||||
// GetMessagesByApplication returns all messages from an application.
|
||||
func (d *GormDatabase) GetMessagesByApplication(tokenID uint) []*model.Message {
|
||||
var messages []*model.Message
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/gotify/server/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func (s *DatabaseSuite) TestMessage() {
|
||||
@@ -34,6 +35,16 @@ func (s *DatabaseSuite) TestMessage() {
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], backupdone)
|
||||
|
||||
msgs = s.db.GetUnreadMessagesByUserSince(user.ID, 100, 0)
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], backupdone)
|
||||
|
||||
s.db.SetMessagesRead([]uint{backupdone.ID})
|
||||
backupdone.Read = true
|
||||
|
||||
msgs = s.db.GetUnreadMessagesByUserSince(user.ID, 100, 0)
|
||||
assert.Len(s.T(), msgs, 0)
|
||||
|
||||
msgs = s.db.GetMessagesByApplication(backupServer.ID)
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], backupdone)
|
||||
@@ -51,6 +62,10 @@ func (s *DatabaseSuite) TestMessage() {
|
||||
assertEquals(s.T(), msgs[0], logindone)
|
||||
assertEquals(s.T(), msgs[1], backupdone)
|
||||
|
||||
msgs = s.db.GetUnreadMessagesByUserSince(user.ID, 100, 0)
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], logindone)
|
||||
|
||||
msgs = s.db.GetMessagesByApplication(backupServer.ID)
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], backupdone)
|
||||
@@ -74,6 +89,11 @@ func (s *DatabaseSuite) TestMessage() {
|
||||
assertEquals(s.T(), msgs[1], logindone)
|
||||
assertEquals(s.T(), msgs[2], backupdone)
|
||||
|
||||
msgs = s.db.GetUnreadMessagesByUserSince(user.ID, 100, 0)
|
||||
assert.Len(s.T(), msgs, 2)
|
||||
assertEquals(s.T(), msgs[0], loginfailed)
|
||||
assertEquals(s.T(), msgs[1], logindone)
|
||||
|
||||
backupfailed := &model.Message{ApplicationID: backupServer.ID, Message: "backup failed", Title: "backup", Priority: 1, Date: time.Now()}
|
||||
s.db.CreateMessage(backupfailed)
|
||||
assert.NotEqual(s.T(), 0, backupfailed.ID)
|
||||
@@ -85,6 +105,15 @@ func (s *DatabaseSuite) TestMessage() {
|
||||
assertEquals(s.T(), msgs[2], logindone)
|
||||
assertEquals(s.T(), msgs[3], backupdone)
|
||||
|
||||
s.db.SetMessagesRead([]uint{loginfailed.ID, logindone.ID})
|
||||
loginfailed.Read = true
|
||||
logindone.Read = true
|
||||
|
||||
msgs = s.db.GetUnreadMessagesByUserSince(user.ID, 100, 0)
|
||||
fmt.Print(msgs)
|
||||
assert.Len(s.T(), msgs, 1)
|
||||
assertEquals(s.T(), msgs[0], backupfailed)
|
||||
|
||||
msgs = s.db.GetMessagesByApplication(loginServer.ID)
|
||||
assert.Len(s.T(), msgs, 2)
|
||||
assertEquals(s.T(), msgs[0], loginfailed)
|
||||
@@ -140,6 +169,10 @@ func (s *DatabaseSuite) TestGetMessagesSince() {
|
||||
assert.Len(s.T(), actual, 50)
|
||||
hasIDInclusiveBetween(s.T(), actual, 1000, 951, 1)
|
||||
|
||||
actual = s.db.GetUnreadMessagesByUserSince(user.ID, 50, 0)
|
||||
assert.Len(s.T(), actual, 50)
|
||||
hasIDInclusiveBetween(s.T(), actual, 1000, 951, 1)
|
||||
|
||||
actual = s.db.GetMessagesByUserSince(user.ID, 50, 951)
|
||||
assert.Len(s.T(), actual, 50)
|
||||
hasIDInclusiveBetween(s.T(), actual, 950, 901, 1)
|
||||
|
||||
123
docs/spec.json
123
docs/spec.json
@@ -753,6 +753,119 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/message/read": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"clientTokenHeader": []
|
||||
},
|
||||
{
|
||||
"clientTokenQuery": []
|
||||
},
|
||||
{
|
||||
"basicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Marks messages as read.",
|
||||
"operationId": "markMessagesAsRead",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": "the message ids",
|
||||
"name": "id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Ok"
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/message/unread": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"clientTokenHeader": []
|
||||
},
|
||||
{
|
||||
"clientTokenQuery": []
|
||||
},
|
||||
{
|
||||
"basicAuth": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"message"
|
||||
],
|
||||
"summary": "Return all unread messages.",
|
||||
"operationId": "getUnreadMessages",
|
||||
"parameters": [
|
||||
{
|
||||
"maximum": 200,
|
||||
"minimum": 1,
|
||||
"type": "integer",
|
||||
"default": 100,
|
||||
"description": "the maximal amount of messages to return",
|
||||
"name": "limit",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"minimum": 0,
|
||||
"type": "integer",
|
||||
"description": "return all messages with an ID less than this value",
|
||||
"name": "since",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Ok",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/PagedMessages"
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/message/{id}": {
|
||||
"delete": {
|
||||
"security": [
|
||||
@@ -1256,7 +1369,8 @@
|
||||
"appid",
|
||||
"message",
|
||||
"title",
|
||||
"date"
|
||||
"date",
|
||||
"read"
|
||||
],
|
||||
"properties": {
|
||||
"appid": {
|
||||
@@ -1296,6 +1410,13 @@
|
||||
"x-go-name": "Priority",
|
||||
"example": 2
|
||||
},
|
||||
"read": {
|
||||
"description": "If the message was already read.",
|
||||
"type": "boolean",
|
||||
"x-go-name": "Read",
|
||||
"readOnly": true,
|
||||
"example": true
|
||||
},
|
||||
"title": {
|
||||
"description": "The title of the message.",
|
||||
"type": "string",
|
||||
|
||||
@@ -40,4 +40,10 @@ type Message struct {
|
||||
// required: true
|
||||
// example: 2018-02-27T19:36:10.5045044+01:00
|
||||
Date time.Time `json:"date"`
|
||||
// If the message was already read.
|
||||
//
|
||||
// read only: true
|
||||
// required: true
|
||||
// example: true
|
||||
Read bool `json:"read"`
|
||||
}
|
||||
|
||||
@@ -462,6 +462,83 @@ func Create(db *database.GormDatabase, vInfo *model.VersionInfo, conf *config.Co
|
||||
// $ref: "#/definitions/Error"
|
||||
message.GET("", messageHandler.GetMessages)
|
||||
|
||||
// swagger:operation GET /message/unread message getUnreadMessages
|
||||
//
|
||||
// Return all unread messages.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// security:
|
||||
// - clientTokenHeader: []
|
||||
// - clientTokenQuery: []
|
||||
// - basicAuth: []
|
||||
// parameters:
|
||||
// - name: limit
|
||||
// in: query
|
||||
// description: the maximal amount of messages to return
|
||||
// required: false
|
||||
// maximum: 200
|
||||
// minimum: 1
|
||||
// default: 100
|
||||
// type: integer
|
||||
// - name: since
|
||||
// in: query
|
||||
// description: return all messages with an ID less than this value
|
||||
// minimum: 0
|
||||
// required: false
|
||||
// type: integer
|
||||
// responses:
|
||||
// 200:
|
||||
// description: Ok
|
||||
// schema:
|
||||
// $ref: "#/definitions/PagedMessages"
|
||||
// 401:
|
||||
// description: Unauthorized
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
// 403:
|
||||
// description: Forbidden
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
message.GET("/unread", messageHandler.GetUnreadMessages)
|
||||
|
||||
// swagger:operation POST /message/read message markMessagesAsRead
|
||||
//
|
||||
// Marks messages as read.
|
||||
//
|
||||
// ---
|
||||
// produces:
|
||||
// - application/json
|
||||
// security:
|
||||
// - clientTokenHeader: []
|
||||
// - clientTokenQuery: []
|
||||
// - basicAuth: []
|
||||
// parameters:
|
||||
// - name: id
|
||||
// in: query
|
||||
// description: the message ids
|
||||
// required: true
|
||||
// type: array
|
||||
// items:
|
||||
// type: integer
|
||||
// responses:
|
||||
// 200:
|
||||
// description: Ok
|
||||
// 401:
|
||||
// description: Unauthorized
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
// 403:
|
||||
// description: Forbidden
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
// 404:
|
||||
// description: Not Found
|
||||
// schema:
|
||||
// $ref: "#/definitions/Error"
|
||||
message.POST("/read", messageHandler.MarkMessagesAsRead)
|
||||
|
||||
// swagger:operation DELETE /message message deleteMessages
|
||||
//
|
||||
// Delete all messages.
|
||||
|
||||
7
ui/load.js
Normal file
7
ui/load.js
Normal file
@@ -0,0 +1,7 @@
|
||||
let id = 0;
|
||||
module.exports = function(requestId) {
|
||||
return {
|
||||
message: requestId,
|
||||
title: ""+(id++)
|
||||
};
|
||||
};
|
||||
17
ui/package-lock.json
generated
17
ui/package-lock.json
generated
@@ -318,6 +318,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-visibility-sensor": {
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-visibility-sensor/-/react-visibility-sensor-3.11.0.tgz",
|
||||
"integrity": "sha512-a5T4bW3tIUkZM8L9CA92nRJzMARRw+aqSs7r2dv8K1NZA1Pot7mHhAUgaKLlrCPZeZ7LynQXIQJhbMiQsZdkGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/rimraf": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.2.tgz",
|
||||
@@ -9814,6 +9823,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-visibility-sensor": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.0.1.tgz",
|
||||
"integrity": "sha512-EVrSpvFN8dD4svTXICejydCd4F5ufTDj4ARRzKYBScaJ/pPsViuYRbeI02SaSySnL2v4/ONt0XveeUhkZbk0gg==",
|
||||
"requires": {
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"react-router-dom": "^4.3.1",
|
||||
"react-scripts-ts": "2.17.0",
|
||||
"react-timeago": "^4.1.9",
|
||||
"react-visibility-sensor": "^5.0.1",
|
||||
"typeface-roboto": "0.0.54",
|
||||
"typeface-roboto-mono": "0.0.54"
|
||||
},
|
||||
@@ -43,6 +44,7 @@
|
||||
"@types/react-dom": "^16.0.7",
|
||||
"@types/react-infinite": "0.0.33",
|
||||
"@types/react-router-dom": "^4.3.0",
|
||||
"@types/react-visibility-sensor": "^3.11.0",
|
||||
"@types/rimraf": "^2.0.2",
|
||||
"get-port": "^4.0.0",
|
||||
"prettier": "^1.14.2",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import {withStyles, WithStyles} from '@material-ui/core/styles';
|
||||
import {StyleRules, withStyles, WithStyles} from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Delete from '@material-ui/icons/Delete';
|
||||
import React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import Container from '../common/Container';
|
||||
|
||||
const styles = () => ({
|
||||
const styles = (): StyleRules => ({
|
||||
header: {
|
||||
display: 'flex',
|
||||
},
|
||||
@@ -31,19 +31,12 @@ const styles = () => ({
|
||||
},
|
||||
});
|
||||
|
||||
type Style = WithStyles<
|
||||
| 'header'
|
||||
| 'headerTitle'
|
||||
| 'trash'
|
||||
| 'wrapperPadding'
|
||||
| 'messageContentWrapper'
|
||||
| 'image'
|
||||
| 'imageWrapper'
|
||||
>;
|
||||
type Style = WithStyles<typeof styles>;
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
image?: string;
|
||||
read: boolean;
|
||||
date: string;
|
||||
content: string;
|
||||
fDelete: VoidFunction;
|
||||
@@ -57,10 +50,20 @@ class Message extends React.PureComponent<IProps & Style> {
|
||||
this.props.height(this.node ? this.node.getBoundingClientRect().height : 0);
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const {fDelete, classes, title, date, content, image} = this.props;
|
||||
const {fDelete, classes, title, date, content, image, read} = this.props;
|
||||
const containerStyle: React.CSSProperties = {display: 'flex', transition: 'all 3s linear'};
|
||||
|
||||
if (read) {
|
||||
containerStyle.borderTop = '0px solid black';
|
||||
containerStyle.padding = '16px';
|
||||
} else {
|
||||
containerStyle.borderTop = '5px solid orange';
|
||||
containerStyle.padding = '11px 16px 16px 16px';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${classes.wrapperPadding} message`} ref={(ref) => (this.node = ref)}>
|
||||
<Container style={{display: 'flex'}}>
|
||||
<Container style={containerStyle}>
|
||||
<div className={classes.imageWrapper}>
|
||||
<img
|
||||
src={image}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {RouteComponentProps} from 'react-router';
|
||||
import DefaultPage from '../common/DefaultPage';
|
||||
import Message from './Message';
|
||||
import {observer} from 'mobx-react';
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import {inject, Stores} from '../inject';
|
||||
import {observable} from 'mobx';
|
||||
import ReactInfinite from 'react-infinite';
|
||||
@@ -103,20 +104,28 @@ class Messages extends Component<IProps & Stores<'messagesStore' | 'appStore'>,
|
||||
this.props.messagesStore.removeSingle(message);
|
||||
|
||||
private renderMessage = (message: IMessage) => {
|
||||
const onVisibilityChange = (visible: boolean) => {
|
||||
if (visible && !message.read) {
|
||||
this.props.messagesStore.markAsRead(message);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Message
|
||||
key={message.id}
|
||||
height={(height) => {
|
||||
if (!this.heights[message.id]) {
|
||||
this.heights[message.id] = height;
|
||||
}
|
||||
}}
|
||||
fDelete={this.deleteMessage(message)}
|
||||
title={message.title}
|
||||
date={message.date}
|
||||
content={message.message}
|
||||
image={message.image}
|
||||
/>
|
||||
// @ts-ignore
|
||||
<VisibilitySensor key={message.id} onChange={onVisibilityChange}>
|
||||
<Message
|
||||
height={(height) => {
|
||||
if (!this.heights[message.id]) {
|
||||
this.heights[message.id] = height;
|
||||
}
|
||||
}}
|
||||
fDelete={this.deleteMessage(message)}
|
||||
title={message.title}
|
||||
date={message.date}
|
||||
content={message.message}
|
||||
read={message.read}
|
||||
image={message.image}
|
||||
/>
|
||||
</VisibilitySensor>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {BaseStore} from '../common/BaseStore';
|
||||
import {action, IObservableArray, observable, reaction} from 'mobx';
|
||||
import {action, IObservableArray, observable, reaction, transaction} from 'mobx';
|
||||
import axios, {AxiosResponse} from 'axios';
|
||||
import * as config from '../config';
|
||||
import {createTransformer} from 'mobx-utils';
|
||||
import {chunkProcessor, createTransformer} from 'mobx-utils';
|
||||
import {SnackReporter} from '../snack/SnackManager';
|
||||
|
||||
const AllMessages = -1;
|
||||
@@ -18,6 +18,12 @@ export class MessagesStore {
|
||||
@observable
|
||||
private state: Record<number, MessagesState> = {};
|
||||
|
||||
@observable
|
||||
private readMessageQueue: number[] = [];
|
||||
|
||||
@observable
|
||||
private newMessageQueue: IMessage[] = [];
|
||||
|
||||
private loading = false;
|
||||
|
||||
public constructor(
|
||||
@@ -25,8 +31,14 @@ export class MessagesStore {
|
||||
private readonly snack: SnackReporter
|
||||
) {
|
||||
reaction(() => appStore.getItems(), this.createEmptyStatesForApps);
|
||||
chunkProcessor(this.readMessageQueue, this.markMessagesAsReadRemote, 1000);
|
||||
chunkProcessor(this.newMessageQueue, (messages) => messages.map(this.showNewMessage), 200);
|
||||
}
|
||||
|
||||
private markMessagesAsReadRemote = (ids: number[]) => {
|
||||
axios.post(config.get('url') + 'message/read?' + ids.map((id) => `id=${id}`).join('&'));
|
||||
};
|
||||
|
||||
private stateOf = (appId: number, create = true) => {
|
||||
if (this.state[appId] || !create) {
|
||||
return this.state[appId] || this.emptyState();
|
||||
@@ -47,17 +59,47 @@ export class MessagesStore {
|
||||
const pagedResult = await this.fetchMessages(appId, state.nextSince).then(
|
||||
(resp) => resp.data
|
||||
);
|
||||
|
||||
state.messages.replace([...state.messages, ...pagedResult.messages]);
|
||||
state.nextSince = pagedResult.paging.since || 0;
|
||||
state.hasMore = 'next' in pagedResult.paging;
|
||||
state.loaded = true;
|
||||
this.loading = false;
|
||||
transaction(() => {
|
||||
state.loaded = true;
|
||||
state.nextSince = pagedResult.paging.since || 0;
|
||||
state.hasMore = 'next' in pagedResult.paging;
|
||||
state.messages.replace([...state.messages, ...pagedResult.messages]);
|
||||
this.loading = false;
|
||||
});
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@action
|
||||
public markAsRead = (message: IMessage) => {
|
||||
console.log('MARK AS READ', this.stateOf(-1).loaded);
|
||||
|
||||
if (this.exists(AllMessages)) {
|
||||
console.log('ALl messages');
|
||||
|
||||
this.markAsReadWithMessages(this.state[AllMessages].messages, message);
|
||||
}
|
||||
if (this.exists(message.appid)) {
|
||||
this.markAsReadWithMessages(this.state[message.appid].messages, message);
|
||||
}
|
||||
this.readMessageQueue.push(message.id);
|
||||
};
|
||||
|
||||
private markAsReadWithMessages = (messages: IMessage[], toUpdate: IMessage) => {
|
||||
const foundMessage = messages.find((message) => toUpdate.id === message.id);
|
||||
console.log('UPDATE MESSAGE', foundMessage);
|
||||
|
||||
if (foundMessage) {
|
||||
foundMessage.read = true;
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
public publishSingleMessage = (message: IMessage) => {
|
||||
this.newMessageQueue.push(message);
|
||||
};
|
||||
|
||||
@action
|
||||
public showNewMessage = (message: IMessage) => {
|
||||
if (this.exists(AllMessages)) {
|
||||
this.stateOf(AllMessages).messages.unshift(message);
|
||||
}
|
||||
@@ -101,15 +143,13 @@ export class MessagesStore {
|
||||
|
||||
public exists = (id: number) => this.stateOf(id).loaded;
|
||||
|
||||
private removeFromList(messages: IMessage[], messageToDelete: IMessage): false | number {
|
||||
private removeFromList(messages: IMessage[], messageToDelete: IMessage) {
|
||||
if (messages) {
|
||||
const index = messages.findIndex((message) => message.id === messageToDelete.id);
|
||||
if (index !== -1) {
|
||||
messages.splice(index, 1);
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private clear = (appId: number) => (this.state[appId] = this.emptyState());
|
||||
|
||||
@@ -20,6 +20,7 @@ interface IMessage {
|
||||
priority: number;
|
||||
date: string;
|
||||
image?: string;
|
||||
read: boolean;
|
||||
}
|
||||
|
||||
interface IPagedMessages {
|
||||
|
||||
Reference in New Issue
Block a user