3
0

Add support for unread in threads

This commit is contained in:
Denis Arh
2018-11-15 11:38:00 +01:00
parent 6e8591ec8f
commit 277b7412d1
18 changed files with 221 additions and 84 deletions
+5 -5
View File
@@ -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,
}
}
+7 -7
View File
@@ -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"`
}
)
+9
View File
@@ -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;
+15
View File
@@ -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
View File
@@ -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 @@
}
]
}
]
]
+16
View File
@@ -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",
+19 -18
View File
@@ -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
+10
View File
@@ -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)
+4
View File
@@ -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)
}
+45
View File
@@ -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"`
+3 -5
View File
@@ -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)
})
}
+23
View File
@@ -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)
+1 -1
View File
@@ -37,7 +37,7 @@ type (
Member *ChannelMember `json:"-" db:"-"`
Members []uint64 `json:"-" db:"-"`
View *ChannelView `json:"-" db:"-"`
Unread *Unread `json:"-" db:"-"`
}
ChannelFilter struct {
-37
View File
@@ -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
}
+38
View File
@@ -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
}