Add support for unread in threads
This commit is contained in:
@@ -128,7 +128,7 @@ func Channel(ch *samTypes.Channel) *outgoing.Channel {
|
||||
Topic: ch.Topic,
|
||||
Type: string(ch.Type),
|
||||
Members: Uint64stoa(ch.Members),
|
||||
View: ChannelView(ch.View),
|
||||
Unread: Unread(ch.Unread),
|
||||
|
||||
CanJoin: ch.CanJoin,
|
||||
CanPart: ch.CanPart,
|
||||
@@ -174,14 +174,14 @@ func ChannelMembers(members samTypes.ChannelMemberSet) *outgoing.ChannelMemberSe
|
||||
return &retval
|
||||
}
|
||||
|
||||
func ChannelView(v *samTypes.ChannelView) *outgoing.ChannelView {
|
||||
func Unread(v *samTypes.Unread) *outgoing.Unread {
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &outgoing.ChannelView{
|
||||
LastMessageID: Uint64toa(v.LastMessageID),
|
||||
NewMessagesCount: v.NewMessagesCount,
|
||||
return &outgoing.Unread{
|
||||
LastMessageID: v.LastMessageID,
|
||||
Count: v.Count,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,13 +31,13 @@ type (
|
||||
|
||||
Channel struct {
|
||||
// Channel to part (nil) for ALL channels
|
||||
ID string `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
Topic string `json:"topic"`
|
||||
Type string `json:"type"`
|
||||
LastMessageID string `json:"lastMessageID"`
|
||||
Members []string `json:"members,omitempty"`
|
||||
View *ChannelView `json:"view,omitempty"`
|
||||
ID string `json:"ID"`
|
||||
Name string `json:"name"`
|
||||
Topic string `json:"topic"`
|
||||
Type string `json:"type"`
|
||||
LastMessageID string `json:"lastMessageID"`
|
||||
Members []string `json:"members,omitempty"`
|
||||
Unread *Unread `json:"unread,omitempty"`
|
||||
|
||||
CanJoin bool `json:"canJoin"`
|
||||
CanPart bool `json:"canPart"`
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package outgoing
|
||||
|
||||
type (
|
||||
ChannelView struct {
|
||||
// Channel to part (nil) for ALL channels
|
||||
LastMessageID string `json:"lastMessageID"`
|
||||
NewMessagesCount uint32 `json:"newMessagesCount"`
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package outgoing
|
||||
|
||||
type (
|
||||
Unread struct {
|
||||
// Channel to part (nil) for ALL channels
|
||||
LastMessageID uint64 `json:"lastMessageID,string,omitempty"`
|
||||
Count uint32 `json:"count"`
|
||||
}
|
||||
)
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,8 @@
|
||||
ALTER TABLE channel_views RENAME TO unreads;
|
||||
|
||||
ALTER TABLE unreads ADD rel_reply_to BIGINT UNSIGNED NOT NULL AFTER rel_channel;
|
||||
ALTER TABLE unreads CHANGE rel_channel rel_channel BIGINT UNSIGNED NOT NULL DEFAULT 0;
|
||||
ALTER TABLE unreads CHANGE rel_user rel_user BIGINT UNSIGNED NOT NULL DEFAULT 0;
|
||||
ALTER TABLE unreads CHANGE rel_last_message_id rel_last_message BIGINT UNSIGNED NOT NULL DEFAULT 0;
|
||||
ALTER TABLE unreads CHANGE new_messages_count count INT UNSIGNED NOT NULL DEFAULT 0;
|
||||
|
||||
@@ -308,6 +308,21 @@ 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
|
||||
|
||||
+17
-1
@@ -390,6 +390,22 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
@@ -619,4 +635,4 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -132,6 +132,22 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"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",
|
||||
|
||||
@@ -13,8 +13,8 @@ type (
|
||||
ChannelViewRepository interface {
|
||||
With(ctx context.Context, db *factory.DB) ChannelViewRepository
|
||||
|
||||
Find(filter *types.ChannelViewFilter) (types.ChannelViewSet, error)
|
||||
Record(userID, channelID, lastMessageID uint64, count uint32) error
|
||||
Find(filter *types.UnreadFilter) (types.UnreadSet, error)
|
||||
Record(userID, channelID, replyTo, lastMessageID uint64, count uint32) error
|
||||
Inc(channelID, userID uint64) error
|
||||
Dec(channelID, userID uint64) error
|
||||
}
|
||||
@@ -26,17 +26,17 @@ type (
|
||||
|
||||
const (
|
||||
// Fetching channel members of all channels a specific user has access to
|
||||
sqlChannelViewsSelect = `SELECT rel_channel, rel_user, new_messages_count, rel_last_message_id
|
||||
FROM channel_views
|
||||
sqlChannelViewsSelect = `SELECT rel_channel, rel_user, count, rel_last_message
|
||||
FROM unreads
|
||||
WHERE true `
|
||||
|
||||
sqlChannelViewsIncCount = `UPDATE channel_views
|
||||
SET new_messages_count = new_messages_count + 1
|
||||
sqlChannelViewsIncCount = `UPDATE unreads
|
||||
SET count = count + 1
|
||||
WHERE rel_channel = ? AND rel_user <> ?`
|
||||
|
||||
sqlChannelViewsDecCount = `UPDATE channel_views
|
||||
SET new_messages_count = new_messages_count - 1
|
||||
WHERE rel_channel = ? AND rel_user <> ? AND new_messages_count > 0`
|
||||
sqlChannelViewsDecCount = `UPDATE unreads
|
||||
SET count = count - 1
|
||||
WHERE rel_channel = ? AND rel_user <> ? AND count > 0`
|
||||
)
|
||||
|
||||
// ChannelView creates new instance of channel member repository
|
||||
@@ -55,9 +55,9 @@ func (r *channelViews) With(ctx context.Context, db *factory.DB) ChannelViewRepo
|
||||
//
|
||||
// If channelID > 0 it returns members of a specific channel
|
||||
// If userID > 0 it returns members of all channels this user is member of
|
||||
func (r *channelViews) Find(filter *types.ChannelViewFilter) (types.ChannelViewSet, error) {
|
||||
func (r *channelViews) Find(filter *types.UnreadFilter) (types.UnreadSet, error) {
|
||||
params := make([]interface{}, 0)
|
||||
vv := types.ChannelViewSet{}
|
||||
vv := types.UnreadSet{}
|
||||
sql := sqlChannelViewsSelect
|
||||
|
||||
if filter != nil {
|
||||
@@ -72,15 +72,16 @@ func (r *channelViews) Find(filter *types.ChannelViewFilter) (types.ChannelViewS
|
||||
}
|
||||
|
||||
// Records channel view
|
||||
func (r *channelViews) Record(userID, channelID, lastMessageID uint64, count uint32) error {
|
||||
mod := &types.ChannelView{
|
||||
ChannelID: channelID,
|
||||
UserID: userID,
|
||||
LastMessageID: lastMessageID,
|
||||
NewMessagesCount: count,
|
||||
func (r *channelViews) Record(userID, channelID, replyTo, lastMessageID uint64, count uint32) error {
|
||||
mod := &types.Unread{
|
||||
ChannelID: channelID,
|
||||
UserID: userID,
|
||||
ReplyTo: replyTo,
|
||||
LastMessageID: lastMessageID,
|
||||
Count: count,
|
||||
}
|
||||
|
||||
return r.db().Replace("channel_views", mod)
|
||||
return r.db().Replace("unreads", mod)
|
||||
}
|
||||
|
||||
// Increments unread (new) message count on a channel for all but one user
|
||||
|
||||
@@ -33,6 +33,7 @@ type MessageAPI interface {
|
||||
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)
|
||||
@@ -49,6 +50,7 @@ type Message struct {
|
||||
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)
|
||||
@@ -101,6 +103,13 @@ 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()
|
||||
@@ -156,6 +165,7 @@ func (mh *Message) MountRoutes(r chi.Router, middlewares ...func(http.Handler) h
|
||||
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,6 +69,10 @@ 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 nil, ctrl.svc.msg.With(ctx).MarkAsUnread(r.MessageID)
|
||||
}
|
||||
|
||||
func (ctrl *Message) PinCreate(ctx context.Context, r *request.MessagePinCreate) (interface{}, error) {
|
||||
return nil, ctrl.svc.msg.With(ctx).Pin(r.MessageID)
|
||||
}
|
||||
|
||||
@@ -316,6 +316,51 @@ 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"`
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/crusttech/crust/internal/auth"
|
||||
@@ -147,7 +146,6 @@ func (svc *channel) preloadMembers(cc types.ChannelSet) (err error) {
|
||||
if mm, err = svc.cmember.Find(&types.ChannelMemberFilter{ComembersOf: userID}); err != nil {
|
||||
return
|
||||
} else {
|
||||
spew.Dump(mm)
|
||||
err = cc.Walk(func(ch *types.Channel) error {
|
||||
ch.Members = mm.MembersOf(ch.ID)
|
||||
ch.Member = mm.FindByChannelID(ch.ID).FindByUserID(userID)
|
||||
@@ -161,11 +159,11 @@ func (svc *channel) preloadMembers(cc types.ChannelSet) (err error) {
|
||||
func (svc *channel) preloadViews(cc types.ChannelSet) error {
|
||||
var userID = auth.GetIdentityFromContext(svc.ctx).Identity()
|
||||
|
||||
if vv, err := svc.cview.Find(&types.ChannelViewFilter{UserID: userID}); err != nil {
|
||||
if vv, err := svc.cview.Find(&types.UnreadFilter{UserID: userID}); err != nil {
|
||||
return err
|
||||
} else {
|
||||
cc.Walk(func(ch *types.Channel) error {
|
||||
ch.View = vv.FindByChannelId(ch.ID)
|
||||
ch.Unread = vv.FindByChannelId(ch.ID)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -744,7 +742,7 @@ func (svc *channel) DeleteMember(channelID uint64, memberIDs ...uint64) (err err
|
||||
|
||||
func (svc *channel) RecordView(userID, channelID, lastMessageID uint64) error {
|
||||
return svc.db.Transaction(func() (err error) {
|
||||
return svc.cview.Record(userID, channelID, lastMessageID, 0)
|
||||
return svc.cview.Record(userID, channelID, lastMessageID, 0, 0)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ type (
|
||||
React(messageID uint64, reaction string) error
|
||||
RemoveReaction(messageID uint64, reaction string) error
|
||||
|
||||
MarkAsUnread(messageID uint64) error
|
||||
|
||||
Pin(messageID uint64) error
|
||||
RemovePin(messageID uint64) error
|
||||
|
||||
@@ -306,6 +308,27 @@ func (svc *message) Delete(ID uint64) error {
|
||||
})
|
||||
}
|
||||
|
||||
// Pin message to the channel
|
||||
func (svc *message) MarkAsUnread(messageID uint64) error {
|
||||
var currentUserID uint64 = repository.Identity(svc.ctx)
|
||||
|
||||
return svc.db.Transaction(func() (err error) {
|
||||
// Broadcast queue
|
||||
var message *types.Message
|
||||
|
||||
message, err = svc.message.FindMessageByID(messageID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if message.ReplyTo > 0 {
|
||||
return svc.cview.Record(currentUserID, message.ChannelID, message.ReplyTo, messageID, 0)
|
||||
} else {
|
||||
return svc.cview.Record(currentUserID, message.ChannelID, 0, messageID, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// React on a message with an emoji
|
||||
func (svc *message) React(messageID uint64, reaction string) error {
|
||||
return svc.flag(messageID, reaction, false)
|
||||
|
||||
@@ -37,7 +37,7 @@ type (
|
||||
|
||||
Member *ChannelMember `json:"-" db:"-"`
|
||||
Members []uint64 `json:"-" db:"-"`
|
||||
View *ChannelView `json:"-" db:"-"`
|
||||
Unread *Unread `json:"-" db:"-"`
|
||||
}
|
||||
|
||||
ChannelFilter struct {
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
package types
|
||||
|
||||
type (
|
||||
ChannelView struct {
|
||||
ChannelID uint64 `db:"rel_channel"`
|
||||
UserID uint64 `db:"rel_user"`
|
||||
LastMessageID uint64 `db:"rel_last_message_id"`
|
||||
|
||||
NewMessagesCount uint32 `db:"new_messages_count"`
|
||||
}
|
||||
|
||||
ChannelViewFilter struct {
|
||||
UserID uint64
|
||||
}
|
||||
|
||||
ChannelViewSet []*ChannelView
|
||||
)
|
||||
|
||||
func (mm ChannelViewSet) Walk(w func(*ChannelView) error) (err error) {
|
||||
for i := range mm {
|
||||
if err = w(mm[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (uu ChannelViewSet) FindByChannelId(channelID uint64) *ChannelView {
|
||||
for i := range uu {
|
||||
if uu[i].ChannelID == channelID {
|
||||
return uu[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package types
|
||||
|
||||
type (
|
||||
Unread struct {
|
||||
ChannelID uint64 `db:"rel_channel"`
|
||||
ReplyTo uint64 `db:"rel_reply_to"`
|
||||
UserID uint64 `db:"rel_user"`
|
||||
LastMessageID uint64 `db:"rel_last_message"`
|
||||
|
||||
Count uint32 `db:"count"`
|
||||
}
|
||||
|
||||
UnreadFilter struct {
|
||||
UserID uint64
|
||||
}
|
||||
|
||||
UnreadSet []*Unread
|
||||
)
|
||||
|
||||
func (mm UnreadSet) Walk(w func(*Unread) error) (err error) {
|
||||
for i := range mm {
|
||||
if err = w(mm[i]); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (uu UnreadSet) FindByChannelId(channelID uint64) *Unread {
|
||||
for i := range uu {
|
||||
if uu[i].ChannelID == channelID {
|
||||
return uu[i]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user