3
0

Unread messages, reworked

This commit is contained in:
Denis Arh
2019-01-17 19:49:40 +01:00
parent 590aab7bd4
commit 14a344c7a1
14 changed files with 193 additions and 134 deletions

View File

@@ -310,6 +310,30 @@
]
}
},
{
"name": "markAsRead",
"path": "/mark-as-read",
"method": "GET",
"title": "Manages read/unread messages in a channel or a thread",
"parameters": {
"path": [
],
"post": [
{
"type": "uint64",
"name": "threadID",
"required": false,
"title": "ID of thread (messageID) "
},
{
"type": "uint64",
"name": "lastReadMessageID",
"required": false,
"title": "ID of the last read message"
}
]
}
},
{
"name": "edit",
"path": "/{messageID}",
@@ -390,22 +414,6 @@
]
}
},
{
"name": "markAsUnread",
"path": "/{messageID}/unread",
"method": "POST",
"title": "Mark message in channel (or thread) as unread",
"parameters": {
"path": [
{
"name": "messageID",
"type": "uint64",
"required": true,
"title": "Message ID"
}
]
}
},
{
"name": "pinCreate",
"path": "/{messageID}/pin",

View File

@@ -52,6 +52,29 @@
]
}
},
{
"Name": "markAsRead",
"Method": "GET",
"Title": "Manages read/unread messages in a channel or a thread",
"Path": "/mark-as-read",
"Parameters": {
"path": [],
"post": [
{
"name": "threadID",
"required": false,
"title": "ID of thread (messageID) ",
"type": "uint64"
},
{
"name": "lastReadMessageID",
"required": false,
"title": "ID of the last read message",
"type": "uint64"
}
]
}
},
{
"Name": "edit",
"Method": "PUT",
@@ -132,22 +155,6 @@
]
}
},
{
"Name": "markAsUnread",
"Method": "POST",
"Title": "Mark message in channel (or thread) as unread",
"Path": "/{messageID}/unread",
"Parameters": {
"path": [
{
"name": "messageID",
"required": true,
"title": "Message ID",
"type": "uint64"
}
]
}
},
{
"Name": "pinCreate",
"Method": "POST",

View File

@@ -246,6 +246,22 @@ The following event types may be sent with a message event:
| lastMessageID | uint64 | GET | | N/A | NO |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
## Manages read/unread messages in a channel or a thread
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/mark-as-read` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
| threadID | uint64 | POST | ID of thread (messageID) | N/A | NO |
| lastReadMessageID | uint64 | POST | ID of the last read message | N/A | NO |
## Edit existing message
#### Method
@@ -308,21 +324,6 @@ The following event types may be sent with a message event:
| channelID | uint64 | PATH | Channel ID | N/A | YES |
| message | string | POST | Message contents (markdown) | N/A | YES |
## Mark message in channel (or thread) as unread
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/channels/{channelID}/messages/{messageID}/unread` | HTTP/S | POST | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| messageID | uint64 | PATH | Message ID | N/A | YES |
| channelID | uint64 | PATH | Channel ID | N/A | YES |
## Pin message to channel (public bookmark)
#### Method

View File

@@ -11,6 +11,7 @@ type (
ChannelID string `json:"id"`
}
// @deprecated
ChannelViewRecord struct {
ChannelID uint64 `json:"channelID,string,omitempty"`
LastMessageID uint64 `json:"lastMessageID,string,omitempty"`

View File

@@ -198,14 +198,18 @@ func (r *message) FindThreads(filter *types.MessageFilter) (types.MessageSet, er
return rval, r.db().Select(&rval, sql, params...)
}
func (r *message) CountFromMessageID(channelID, threadID, messageID uint64) (uint32, error) {
func (r *message) CountFromMessageID(channelID, threadID, lastReadMessageID uint64) (uint32, error) {
if lastReadMessageID == 0 {
// No need for counting, zero unread messages...
return 0, nil
}
rval := struct{ Count uint32 }{}
return rval.Count, r.db().Get(&rval,
sqlCountFromMessageID,
channelID,
threadID,
types.MessageTypeChannelEvent,
messageID,
lastReadMessageID,
)
}

View File

@@ -14,7 +14,7 @@ type (
With(ctx context.Context, db *factory.DB) UnreadRepository
Find(filter *types.UnreadFilter) (types.UnreadSet, error)
Record(userID, channelID, replyTo, lastMessageID uint64, count uint32) error
Record(userID, channelID, threadID, lastReadMessageID uint64, count uint32) error
Inc(channelID, replyTo, userID uint64) error
Dec(channelID, replyTo, userID uint64) error
}
@@ -72,26 +72,26 @@ func (r *unread) Find(filter *types.UnreadFilter) (types.UnreadSet, error) {
}
// Records channel view
func (r *unread) Record(userID, channelID, replyTo, lastMessageID uint64, count uint32) error {
func (r *unread) Record(userID, channelID, threadID, lastReadMessageID uint64, count uint32) error {
mod := &types.Unread{
ChannelID: channelID,
UserID: userID,
ReplyTo: replyTo,
LastMessageID: lastMessageID,
ReplyTo: threadID,
LastMessageID: lastReadMessageID,
Count: count,
}
return r.db().Replace("unreads", mod)
}
// Increments unread (new) message count on a channel for all but one user
func (r *unread) Inc(channelID, replyTo, userID uint64) error {
_, err := r.db().Exec(sqlUnreadIncCount, channelID, replyTo, userID)
// Increments unread message count on a channel/thread for all but one user
func (r *unread) Inc(channelID, threadID, userID uint64) error {
_, err := r.db().Exec(sqlUnreadIncCount, channelID, threadID, userID)
return err
}
// Increments unread (new) message count on a channel for all but one user
func (r *unread) Dec(channelID, replyTo, userID uint64) error {
_, err := r.db().Exec(sqlUnreadDecCount, channelID, replyTo, userID)
// Decrements unread message count on a channel/thread for all but one user
func (r *unread) Dec(channelID, threadID, userID uint64) error {
_, err := r.db().Exec(sqlUnreadDecCount, channelID, threadID, userID)
return err
}

View File

@@ -29,11 +29,11 @@ import (
type MessageAPI interface {
Create(context.Context, *request.MessageCreate) (interface{}, error)
History(context.Context, *request.MessageHistory) (interface{}, error)
MarkAsRead(context.Context, *request.MessageMarkAsRead) (interface{}, error)
Edit(context.Context, *request.MessageEdit) (interface{}, error)
Delete(context.Context, *request.MessageDelete) (interface{}, error)
ReplyGet(context.Context, *request.MessageReplyGet) (interface{}, error)
ReplyCreate(context.Context, *request.MessageReplyCreate) (interface{}, error)
MarkAsUnread(context.Context, *request.MessageMarkAsUnread) (interface{}, error)
PinCreate(context.Context, *request.MessagePinCreate) (interface{}, error)
PinRemove(context.Context, *request.MessagePinRemove) (interface{}, error)
BookmarkCreate(context.Context, *request.MessageBookmarkCreate) (interface{}, error)
@@ -46,11 +46,11 @@ type MessageAPI interface {
type Message struct {
Create func(http.ResponseWriter, *http.Request)
History func(http.ResponseWriter, *http.Request)
MarkAsRead func(http.ResponseWriter, *http.Request)
Edit func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
ReplyGet func(http.ResponseWriter, *http.Request)
ReplyCreate func(http.ResponseWriter, *http.Request)
MarkAsUnread func(http.ResponseWriter, *http.Request)
PinCreate func(http.ResponseWriter, *http.Request)
PinRemove func(http.ResponseWriter, *http.Request)
BookmarkCreate func(http.ResponseWriter, *http.Request)
@@ -75,6 +75,13 @@ func NewMessage(mh MessageAPI) *Message {
return mh.History(r.Context(), params)
})
},
MarkAsRead: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageMarkAsRead()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.MarkAsRead(r.Context(), params)
})
},
Edit: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageEdit()
@@ -103,13 +110,6 @@ func NewMessage(mh MessageAPI) *Message {
return mh.ReplyCreate(r.Context(), params)
})
},
MarkAsUnread: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessageMarkAsUnread()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return mh.MarkAsUnread(r.Context(), params)
})
},
PinCreate: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewMessagePinCreate()
@@ -161,11 +161,11 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
r.Post("/", mh.Create)
r.Get("/", mh.History)
r.Get("/mark-as-read", mh.MarkAsRead)
r.Put("/{messageID}", mh.Edit)
r.Delete("/{messageID}", mh.Delete)
r.Get("/{messageID}/replies", mh.ReplyGet)
r.Post("/{messageID}/replies", mh.ReplyCreate)
r.Post("/{messageID}/unread", mh.MarkAsUnread)
r.Post("/{messageID}/pin", mh.PinCreate)
r.Delete("/{messageID}/pin", mh.PinRemove)
r.Post("/{messageID}/bookmark", mh.BookmarkCreate)

View File

@@ -69,8 +69,8 @@ func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (inte
return nil, ctrl.svc.msg.With(ctx).Delete(r.MessageID)
}
func (ctrl *Message) MarkAsUnread(ctx context.Context, r *request.MessageMarkAsUnread) (interface{}, error) {
return ctrl.svc.msg.With(ctx).MarkAsUnread(r.MessageID)
func (ctrl *Message) MarkAsRead(ctx context.Context, r *request.MessageMarkAsRead) (interface{}, error) {
return ctrl.svc.msg.With(ctx).MarkAsRead(r.ChannelID, r.ThreadID, r.LastReadMessageID)
}
func (ctrl *Message) PinCreate(ctx context.Context, r *request.MessagePinCreate) (interface{}, error) {

View File

@@ -125,6 +125,59 @@ func (m *MessageHistory) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageHistory()
// Message markAsRead request parameters
type MessageMarkAsRead struct {
ChannelID uint64 `json:",string"`
ThreadID uint64 `json:",string"`
LastReadMessageID uint64 `json:",string"`
}
func NewMessageMarkAsRead() *MessageMarkAsRead {
return &MessageMarkAsRead{}
}
func (m *MessageMarkAsRead) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
if val, ok := post["threadID"]; ok {
m.ThreadID = parseUInt64(val)
}
if val, ok := post["lastReadMessageID"]; ok {
m.LastReadMessageID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewMessageMarkAsRead()
// Message edit request parameters
type MessageEdit struct {
MessageID uint64 `json:",string"`
@@ -315,51 +368,6 @@ func (m *MessageReplyCreate) Fill(r *http.Request) (err error) {
var _ RequestFiller = NewMessageReplyCreate()
// Message markAsUnread request parameters
type MessageMarkAsUnread struct {
MessageID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
}
func NewMessageMarkAsUnread() *MessageMarkAsUnread {
return &MessageMarkAsUnread{}
}
func (m *MessageMarkAsUnread) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(m)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
m.MessageID = parseUInt64(chi.URLParam(r, "messageID"))
m.ChannelID = parseUInt64(chi.URLParam(r, "channelID"))
return err
}
var _ RequestFiller = NewMessageMarkAsUnread()
// Message pinCreate request parameters
type MessagePinCreate struct {
MessageID uint64 `json:",string"`

View File

@@ -740,6 +740,8 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err
})
}
// RecordView
// @deprecated
func (svc *channel) RecordView(userID, channelID, lastMessageID uint64) error {
return svc.db.Transaction(func() (err error) {
return svc.unread.Record(userID, channelID, 0, lastMessageID, 0)

View File

@@ -43,7 +43,7 @@ type (
React(messageID uint64, reaction string) error
RemoveReaction(messageID uint64, reaction string) error
MarkAsUnread(messageID uint64) (uint32, error)
MarkAsRead(channelID, threadID, lastReadMessageID uint64) (uint32, error)
Pin(messageID uint64) error
RemovePin(messageID uint64) error
@@ -308,30 +308,50 @@ func (svc *message) Delete(ID uint64) error {
})
}
// Pin message to the channel
func (svc *message) MarkAsUnread(messageID uint64) (count uint32, err error) {
// M
func (svc *message) MarkAsRead(channelID, threadID, lastReadMessageID uint64) (count uint32, err error) {
var currentUserID uint64 = repository.Identity(svc.ctx)
return count, svc.db.Transaction(func() (err error) {
// Broadcast queue
var message *types.Message
err = svc.db.Transaction(func() (err error) {
var ch *types.Channel
var thread *types.Message
var lastMessage *types.Message
message, err = svc.message.FindMessageByID(messageID)
// Validate channel
if ch, err = svc.channel.FindChannelByID(channelID); err != nil {
return errors.Wrap(err, "unable to verify channel")
} else if !ch.IsValid() {
return errors.New("invalid channel")
}
if threadID > 0 {
// Validate thread
if thread, err = svc.message.FindMessageByID(threadID); err != nil {
return errors.Wrap(err, "unable to verify thread")
} else if !thread.IsValid() {
return errors.New("invalid thread")
}
}
if lastReadMessageID > 0 {
// Validate thread
if lastMessage, err = svc.message.FindMessageByID(lastReadMessageID); err != nil {
return errors.Wrap(err, "unable to verify last message")
} else if !lastMessage.IsValid() {
return errors.New("invalid message")
}
}
count, err = svc.message.CountFromMessageID(channelID, threadID, lastReadMessageID)
if err != nil {
return err
return errors.Wrap(err, "unable to count unread messages")
}
count, err = svc.message.CountFromMessageID(message.ChannelID, message.ReplyTo, message.ID)
if err != nil {
return
}
if message.ReplyTo > 0 {
return svc.unreads.Record(currentUserID, message.ChannelID, message.ReplyTo, messageID, count)
} else {
return svc.unreads.Record(currentUserID, message.ChannelID, 0, messageID, count)
}
err = svc.unreads.Record(currentUserID, channelID, threadID, lastReadMessageID, count)
return errors.Wrap(err, "unable to record unread messages")
})
return count, errors.Wrap(err, "unable to mark as read")
}
// React on a message with an emoji

View File

@@ -122,3 +122,7 @@ func (mtype MessageType) Value() (driver.Value, error) {
return mtype.String(), nil
}
func (m *Message) IsValid() bool {
return m.DeletedAt == nil
}

View File

@@ -37,8 +37,11 @@ func (s *Session) dispatch(raw []byte) error {
return s.channelCreate(ctx, p.ChannelCreate)
case p.ChannelUpdate != nil:
return s.channelUpdate(ctx, p.ChannelUpdate)
// @deprecated
case p.ChannelViewRecord != nil:
return s.channelViewRecord(ctx, p.ChannelViewRecord)
case p.ChannelActivity != nil:
return s.channelActivity(ctx, p.ChannelActivity)
case p.MessageActivity != nil:

View File

@@ -102,6 +102,7 @@ func (s *Session) channelUpdate(ctx context.Context, p *incoming.ChannelUpdate)
return err
}
// @deprecated
func (s *Session) channelViewRecord(ctx context.Context, p *incoming.ChannelViewRecord) error {
var userID = auth.GetIdentityFromContext(ctx).Identity()