feature(@desktop/chat) Enhance message context menu with mark as unread (#4382)

fixes #10329

linked with PR #12879

- Adds capacity to mark a message as unread
- Adds capacity to mark a message with mention as unread
- Adds persistence to the marking of the message (change can be seen at
  after reboot)
- Adds marking in right click contextual menu
This commit is contained in:
Godfrain Jacques 2023-12-11 00:17:01 -08:00 committed by GitHub
parent be8568e174
commit 0f98a1294c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 512 additions and 2 deletions

View File

@ -1891,6 +1891,99 @@ func (db sqlitePersistence) MarkMessagesSeen(chatID string, ids []string) (uint6
return countWithMentions + countNoMentions, countWithMentions, err
}
func (db sqlitePersistence) GetMessageIdsWithGreaterTimestamp(chatID string, messageID string) ([]string, error) {
var err error
var rows *sql.Rows
query := "SELECT id FROM user_messages WHERE local_chat_id = ? AND timestamp >= (SELECT timestamp FROM user_messages WHERE id = ?)"
rows, err = db.db.Query(query, chatID, messageID)
if err != nil {
return nil, err
}
defer rows.Close()
var ids []string
for rows.Next() {
var messageID string
err = rows.Scan(&messageID)
if err != nil {
return nil, err
}
ids = append(ids, messageID)
}
return ids, nil
}
func (db sqlitePersistence) MarkMessageAsUnread(chatID string, messageID string) (uint64, uint64, error) {
tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{})
if err != nil {
return 0, 0, err
}
defer func() {
if err == nil {
err = tx.Commit()
return
}
// don't shadow original error
_ = tx.Rollback()
}()
// TODO : Reduce number of queries for getting (total unread messages, total messages with mention)
// The function expected result is a pair (total unread messages, total messages with mention)
// Currently a 2 step operation is needed to obtain this pair
_, err = tx.Exec(`UPDATE user_messages SET seen = 1 WHERE local_chat_id = ? AND NOT(seen)`, chatID)
if err != nil {
return 0, 0, err
}
_, err = tx.Exec(
`UPDATE user_messages
SET seen = 0
WHERE local_chat_id = ?
AND seen = 1
AND (mentioned OR replied)
AND timestamp >= (SELECT timestamp FROM user_messages WHERE id = ?)`, chatID, messageID)
if err != nil {
return 0, 0, err
}
var countWithMentions uint64
row := tx.QueryRow("SELECT changes();")
if err := row.Scan(&countWithMentions); err != nil {
return 0, 0, err
}
_, err = tx.Exec(
`UPDATE user_messages
SET seen = 0
WHERE local_chat_id = ?
AND seen = 1
AND NOT(mentioned OR replied)
AND timestamp >= (SELECT timestamp FROM user_messages WHERE id = ?)`, chatID, messageID)
if err != nil {
return 0, 0, err
}
var countNoMentions uint64
row = tx.QueryRow("SELECT changes();")
if err := row.Scan(&countNoMentions); err != nil {
return 0, 0, err
}
count := countWithMentions + countNoMentions
_, err = tx.Exec(
`UPDATE chats
SET unviewed_message_count = ?, unviewed_mentions_count = ?,
highlight = 0
WHERE id = ?`, count, countWithMentions, chatID)
return count, countWithMentions, err
}
func (db sqlitePersistence) UpdateMessageOutgoingStatus(id string, newOutgoingStatus string) error {
_, err := db.db.Exec(`
UPDATE user_messages

View File

@ -4142,6 +4142,49 @@ func (m *Messenger) DeleteMessagesByChatID(id string) error {
return m.persistence.DeleteMessagesByChatID(id)
}
func (m *Messenger) MarkMessageAsUnreadImpl(chatID string, messageID string) (uint64, uint64, error) {
count, countWithMentions, err := m.persistence.MarkMessageAsUnread(chatID, messageID)
if err != nil {
return 0, 0, err
}
chat, err := m.persistence.Chat(chatID)
if err != nil {
return 0, 0, err
}
m.allChats.Store(chatID, chat)
return count, countWithMentions, nil
}
func (m *Messenger) MarkMessageAsUnread(chatID string, messageID string) (uint64, uint64, []*ActivityCenterNotification, error) {
count, countWithMentions, err := m.MarkMessageAsUnreadImpl(chatID, messageID)
if err != nil {
return 0, 0, nil, err
}
ids, err := m.persistence.GetMessageIdsWithGreaterTimestamp(chatID, messageID)
if err != nil {
return 0, 0, nil, err
}
hexBytesIds := []types.HexBytes{}
for _, id := range ids {
hexBytesIds = append(hexBytesIds, types.FromHex(id))
}
updatedAt := m.GetCurrentTimeInMillis()
notifications, err := m.persistence.MarkActivityCenterNotificationsUnread(hexBytesIds, updatedAt)
if err != nil {
return 0, 0, nil, err
}
return count, countWithMentions, notifications, nil
}
// MarkMessagesSeen marks messages with `ids` as seen in the chat `chatID`.
// It returns the number of affected messages or error. If there is an error,
// the number of affected messages is always zero.

View File

@ -6,7 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"math/big"
"os"
"strings"
@ -2474,7 +2474,7 @@ func buildImageWithAlbumIDMessage(chat Chat, albumID string) (*common.Message, e
}
defer file.Close()
payload, err := ioutil.ReadAll(file)
payload, err := io.ReadAll(file)
if err != err {
return nil, err
}

View File

@ -0,0 +1,360 @@
package protocol
import (
"context"
"errors"
_ "github.com/mutecomm/go-sqlcipher/v4" // require go-sqlcipher that overrides default implementation
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
"github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt"
)
func (s *MessengerSuite) checkMessageSeen(messageID string, expectedSeen bool) {
message, err := s.m.MessageByID(messageID)
s.Require().NoError(err)
s.Require().Equal(expectedSeen, message.Seen)
}
func (s *MessengerSuite) retrieveAllWithRetry(errorMessage string) (*MessengerResponse, error) {
var response *MessengerResponse
var err error
retryFunc := func() error {
response, err = s.m.RetrieveAll()
if err != nil {
return err
}
if len(response.messages) == 0 {
return errors.New(errorMessage)
}
return nil
}
err = tt.RetryWithBackOff(retryFunc)
return response, err
}
func (s *MessengerSuite) TestMarkMessageAsUnreadWhenMessageListContainsSingleMessage() {
chat := CreatePublicChat("test-chat-1", s.m.transport)
chat.UnviewedMessagesCount = 2
chat.UnviewedMentionsCount = 2
chat.Highlight = true
err := s.m.SaveChat(chat)
s.Require().NoError(err)
inputMessage1 := buildTestMessage(*chat)
inputMessage1.ID = "1"
inputMessage1.Seen = true
inputMessage1.Mentioned = true
err = s.m.SaveMessages([]*common.Message{inputMessage1})
s.Require().NoError(err)
_, err = s.m.MarkAllRead(context.Background(), chat.ID)
s.Require().NoError(err)
chats := s.m.allChats
actualChat, ok := chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(0), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, true)
count, countWithMentions, notifications, err := s.m.MarkMessageAsUnread(chat.ID, inputMessage1.ID)
s.Require().NoError(err)
s.Require().Equal(uint64(1), count)
s.Require().Equal(uint64(1), countWithMentions)
s.Require().Len(notifications, 0)
chats = s.m.allChats
actualChat, ok = chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(1), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(1), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, false)
}
func (s *MessengerSuite) TestMarkMessageAsUnreadWhenMessageListContainsSeveralMessages() {
chat := CreatePublicChat("test-chat-2", s.m.transport)
chat.UnviewedMessagesCount = 0
chat.UnviewedMentionsCount = 0
chat.Highlight = true
err := s.m.SaveChat(chat)
s.Require().NoError(err)
inputMessage1 := buildTestMessage(*chat)
inputMessage1.ID = "1"
inputMessage1.Seen = true
inputMessage1.Mentioned = true
inputMessage1.Timestamp = 1
inputMessage2 := buildTestMessage(*chat)
inputMessage2.ID = "2"
inputMessage2.Seen = true
inputMessage2.Mentioned = false
inputMessage2.Timestamp = 2
inputMessage3 := buildTestMessage(*chat)
inputMessage3.ID = "3"
inputMessage3.Seen = true
inputMessage3.Mentioned = false
inputMessage3.Timestamp = 3
err = s.m.SaveMessages([]*common.Message{
inputMessage1,
inputMessage2,
inputMessage3,
})
s.Require().NoError(err)
_, err = s.m.MarkAllRead(context.Background(), chat.ID)
s.Require().NoError(err)
chats := s.m.allChats
actualChat, ok := chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(0), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, true)
s.checkMessageSeen(inputMessage2.ID, true)
s.checkMessageSeen(inputMessage3.ID, true)
count, countWithMentions, notifications, err := s.m.MarkMessageAsUnread(chat.ID, inputMessage2.ID)
s.Require().NoError(err)
// count is 2 because the messages are layout the following way :
//
// inputMessage1 read <-- mentioned
// ----------------------------- <-- marker
// inputMessage2 unread
// inputMessage3 unread
//
// And the inputMessage3 has greater timestamp than inputMessage2
// Similarly, the mentioned message is inputMessage1, therefore the
// countWithMentions is 0
s.Require().Equal(uint64(2), count)
s.Require().Equal(uint64(0), countWithMentions)
s.Require().Len(notifications, 0)
chats = s.m.allChats
actualChat, ok = chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(2), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, true)
s.checkMessageSeen(inputMessage2.ID, false)
s.checkMessageSeen(inputMessage3.ID, false)
}
func (s *MessengerSuite) TestMarkMessageAsUnreadWhenMessageIsAlreadyInUnreadState() {
chat := CreatePublicChat("test-chat-3", s.m.transport)
chat.UnviewedMessagesCount = 1
chat.UnviewedMentionsCount = 0
chat.Highlight = true
err := s.m.SaveChat(chat)
s.Require().NoError(err)
inputMessage1 := buildTestMessage(*chat)
inputMessage1.ID = "1"
inputMessage1.Seen = false
inputMessage1.Mentioned = false
err = s.m.SaveMessages([]*common.Message{inputMessage1})
s.Require().NoError(err)
chats := s.m.allChats
actualChat, ok := chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(1), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().True(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, false)
count, countWithMentions, notifications, err := s.m.MarkMessageAsUnread(chat.ID, inputMessage1.ID)
s.Require().NoError(err)
s.Require().Equal(uint64(1), count)
s.Require().Equal(uint64(0), countWithMentions)
s.Require().Len(notifications, 0)
chats = s.m.allChats
actualChat, ok = chats.Load(chat.ID)
s.Require().True(ok)
s.Require().Equal(uint(1), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, false)
}
func (s *MessengerSuite) TestMarkMessageAsUnreadInOneChatDoesNotImpactOtherChats() {
chat1 := CreatePublicChat("test-chat-1", s.m.transport)
err := s.m.SaveChat(chat1)
s.Require().NoError(err)
chat2 := CreatePublicChat("test-chat-2", s.m.transport)
err = s.m.SaveChat(chat2)
s.Require().NoError(err)
chat1.UnviewedMessagesCount = 2
chat1.UnviewedMentionsCount = 0
chat1.Highlight = true
inputMessage1 := buildTestMessage(*chat1)
inputMessage1.ID = "1"
inputMessage1.Seen = true
inputMessage1.Mentioned = false
inputMessage2 := buildTestMessage(*chat1)
inputMessage2.ID = "2"
inputMessage2.Seen = true
inputMessage2.Mentioned = false
err = s.m.SaveMessages([]*common.Message{inputMessage1, inputMessage2})
s.Require().NoError(err)
chat2.UnviewedMessagesCount = 1
chat2.UnviewedMentionsCount = 0
chat2.Highlight = true
inputMessage3 := buildTestMessage(*chat2)
inputMessage3.ID = "3"
inputMessage3.Seen = false
inputMessage3.Mentioned = false
err = s.m.SaveMessages([]*common.Message{inputMessage3})
s.Require().NoError(err)
_, err = s.m.MarkAllRead(context.Background(), chat1.ID)
s.Require().NoError(err)
s.checkMessageSeen(inputMessage1.ID, true)
s.checkMessageSeen(inputMessage2.ID, true)
count, countWithMentions, notifications, err := s.m.MarkMessageAsUnread(chat1.ID, inputMessage1.ID)
s.Require().NoError(err)
s.Require().Equal(uint(1), chat2.UnviewedMessagesCount)
s.Require().Equal(uint64(2), count)
s.Require().Equal(uint64(0), countWithMentions)
s.Require().Len(notifications, 0)
chats := s.m.allChats
actualChat, ok := chats.Load(chat1.ID)
s.Require().True(ok)
s.Require().Equal(uint(2), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().False(actualChat.Highlight)
s.checkMessageSeen(inputMessage1.ID, false)
s.checkMessageSeen(inputMessage2.ID, false)
actualChat, ok = chats.Load(chat2.ID)
s.Require().True(ok)
s.Require().Equal(uint(1), actualChat.UnviewedMessagesCount)
s.Require().Equal(uint(0), actualChat.UnviewedMentionsCount)
s.Require().True(actualChat.Highlight)
s.checkMessageSeen(inputMessage3.ID, false)
}
func (s *MessengerSuite) TestMarkMessageWithNotificationAsUnreadInCommunityChatSetsNotificationAsUnread() {
other := s.newMessenger()
description := &requests.CreateCommunity{
Membership: protobuf.CommunityPermissions_AUTO_ACCEPT,
Name: "status",
Color: "#ffffff",
Description: "This is just a test description for the community",
}
inviteMessage := "You are invited to community testing message"
response, err := other.CreateCommunity(description, true)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Communities(), 1)
community := response.Communities()[0]
communityChat := response.Chats()[0]
_, err = community.AddMember(&s.m.identity.PublicKey, []protobuf.CommunityMember_Roles{})
s.Require().NoError(err)
err = other.communitiesManager.SaveCommunity(community)
s.Require().NoError(err)
response, err = other.ShareCommunity(
&requests.ShareCommunity{
CommunityID: community.ID(),
Users: []types.HexBytes{common.PubkeyToHexBytes(&s.m.identity.PublicKey)},
InviteMessage: inviteMessage,
},
)
s.Require().NoError(err)
s.Require().NotNil(response)
s.Require().Len(response.Messages(), 1)
response, err = s.retrieveAllWithRetry("community link not received")
s.Require().NoError(err)
s.Require().Len(response.Messages(), 1)
message := response.Messages()[0]
s.Require().Equal(community.IDString(), message.CommunityID)
s.Require().Equal(inviteMessage, message.Text)
communityInResponse := response.Communities()[0]
s.Require().Equal(community.ID(), communityInResponse.ID())
s.Require().True(communityInResponse.Joined())
inputMessage1 := buildTestMessage(*communityChat)
inputMessage1.ChatId = communityChat.ID
inputMessage1.ContentType = protobuf.ChatMessage_TEXT_PLAIN
inputMessage1.Text = "Hello @" + common.EveryoneMentionTag + " !"
sendResponse, err := other.SendChatMessage(context.Background(), inputMessage1)
s.NoError(err)
s.Require().Len(sendResponse.Messages(), 1)
response, err = s.retrieveAllWithRetry("message from other chatter not received")
s.Require().NoError(err)
s.Require().Len(response.Chats(), 1)
s.Require().Len(response.Messages(), 1)
s.Require().Len(response.ActivityCenterNotifications(), 1)
s.Require().False(response.ActivityCenterNotifications()[0].Read)
response, err = s.m.MarkAllRead(context.Background(), communityChat.ID)
s.Require().NoError(err)
s.Require().Len(response.ActivityCenterNotifications(), 1)
s.Require().True(response.ActivityCenterNotifications()[0].Read)
count, countWithMentions, notifications, err := s.m.MarkMessageAsUnread(communityChat.ID, inputMessage1.ID)
s.Require().NoError(err)
s.Require().Equal(count, uint64(1))
s.Require().Equal(countWithMentions, uint64(1))
s.Require().Len(notifications, 1)
s.Require().False(notifications[0].Read)
}

View File

@ -811,6 +811,20 @@ func (api *PublicAPI) MarkMessagesSeen(chatID string, ids []string) (*MarkMessag
return response, nil
}
func (api *PublicAPI) MarkMessageAsUnread(chatID string, messageID string) (*MarkMessageSeenResponse, error) {
count, withMentions, notifications, err := api.service.messenger.MarkMessageAsUnread(chatID, messageID)
if err != nil {
return nil, err
}
response := &MarkMessageSeenResponse{
Count: count,
CountWithMentions: withMentions,
ActivityCenterNotifications: notifications,
}
return response, nil
}
func (api *PublicAPI) MarkAllRead(ctx context.Context, chatID string) (*protocol.MessengerResponse, error) {
return api.service.messenger.MarkAllRead(ctx, chatID)
}