Unread messages, reworked
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -11,6 +11,7 @@ type (
|
||||
ChannelID string `json:"id"`
|
||||
}
|
||||
|
||||
// @deprecated
|
||||
ChannelViewRecord struct {
|
||||
ChannelID uint64 `json:"channelID,string,omitempty"`
|
||||
LastMessageID uint64 `json:"lastMessageID,string,omitempty"`
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -122,3 +122,7 @@ func (mtype MessageType) Value() (driver.Value, error) {
|
||||
|
||||
return mtype.String(), nil
|
||||
}
|
||||
|
||||
func (m *Message) IsValid() bool {
|
||||
return m.DeletedAt == nil
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user