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

* chore: bump status-go

* feature(@desktop/chat) Enhance message context menu with mark as unread
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 20:16:06 -06:00 committed by GitHub
parent fd638de880
commit 7a5e691c90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 331 additions and 12 deletions

View File

@ -111,6 +111,12 @@ proc init*(self: Controller) =
return
self.delegate.onUnpinMessage(args.messageId)
self.events.on(SIGNAL_MESSAGE_MARKED_AS_UNREAD) do(e:Args):
let args = MessageMarkMessageAsUnreadArgs(e)
if (self.chatId != args.chatId):
return
self.delegate.onMarkMessageAsUnread(args.messageId)
self.events.on(SIGNAL_MESSAGE_REACTION_ADDED) do(e:Args):
let args = MessageAddRemoveReactionArgs(e)
if(self.chatId != args.chatId):
@ -267,6 +273,9 @@ proc removeReaction*(self: Controller, messageId: string, emojiId: int, reaction
proc pinUnpinMessage*(self: Controller, messageId: string, pin: bool) =
self.messageService.pinUnpinMessage(self.chatId, messageId, pin)
proc markMessageAsUnread*(self: Controller, messageId: string) =
self.messageService.asyncMarkMessageAsUnread(self.chatId, messageId)
proc getContactById*(self: Controller, contactId: string): ContactsDto =
return self.contactService.getContactById(contactId)

View File

@ -48,6 +48,12 @@ method onPinMessage*(self: AccessInterface, messageId: string, actionInitiatedBy
method onUnpinMessage*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method markMessageAsUnread*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onMarkMessageAsUnread*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method messagesAdded*(self: AccessInterface, messages: seq[MessageDto]) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -510,12 +510,18 @@ method toggleReactionFromOthers*(self: Module, messageId: string, emojiId: int,
method pinUnpinMessage*(self: Module, messageId: string, pin: bool) =
self.controller.pinUnpinMessage(messageId, pin)
method markMessageAsUnread*(self: Module, messageId: string) =
self.controller.markMessageAsUnread(messageId)
method onPinMessage*(self: Module, messageId: string, actionInitiatedBy: string) =
self.view.model().pinUnpinMessage(messageId, true, actionInitiatedBy)
method onUnpinMessage*(self: Module, messageId: string) =
self.view.model().pinUnpinMessage(messageId, false, "")
method onMarkMessageAsUnread*(self: Module, messageId: string) =
self.view.model().markMessageAsUnread(messageId)
method getSectionId*(self: Module): string =
return self.controller.getMySectionId()

View File

@ -17,6 +17,7 @@ QtObject:
chatIcon: string
chatType: int
loading: bool
keepUnread: bool
proc delete*(self: View) =
self.model.delete
@ -36,6 +37,7 @@ QtObject:
result.chatIcon = ""
result.chatType = ChatType.Unknown.int
result.loading = false
result.keepUnread = false
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -57,6 +59,25 @@ QtObject:
proc unpinMessage*(self: View, messageId: string) {.slot.} =
self.delegate.pinUnpinMessage(messageId, false)
proc keepUnreadChanged*(self: View) {.signal.}
proc getKeepUnread*(self: View): bool {.slot.} =
return self.keepUnread
QtProperty[bool] keepUnread:
read = getKeepUnread
notify = keepUnreadChanged
proc setKeepUnread*(self: View, value: bool) =
self.keepUnread = value
self.keepUnreadChanged()
proc markMessageAsUnread*(self: View, messageId: string) {.slot.} =
self.delegate.markMessageAsUnread(messageId)
self.setKeepUnread(true)
proc updateKeepUnread*(self: View, flag: bool) {.slot.} =
self.setKeepUnread(flag)
proc getMessageByIdAsJson*(self: View, messageId: string): string {.slot.} =
let jsonObj = self.model.getMessageByIdAsJson(messageId)
if(jsonObj.isNil):

View File

@ -135,6 +135,12 @@ proc init*(self: Controller) =
self.chatService.updateUnreadMessagesAndMentions(args.chatId, args.allMessagesMarked, args.messagesCount, args.messagesWithMentionsCount)
self.delegate.onMarkAllMessagesRead(chat)
self.events.on(message_service.SIGNAL_MESSAGE_MARKED_AS_UNREAD) do(e:Args):
let args = message_service.MessageMarkMessageAsUnreadArgs(e)
let chat = self.chatService.getChatById(args.chatId)
self.delegate.onMarkMessageAsUnread(chat)
self.events.on(chat_service.SIGNAL_CHAT_LEFT) do(e: Args):
let args = chat_service.ChatArgs(e)
self.delegate.onCommunityChannelDeletedOrChatLeft(args.chatId)

View File

@ -94,6 +94,9 @@ method changeMutedOnChat*(self: AccessInterface, chatId: string, muted: bool) {.
method onMarkAllMessagesRead*(self: AccessInterface, chat: ChatDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onMarkMessageAsUnread*(self: AccessInterface, chat: ChatDto) {.base.} =
raise newException(ValueError, "No implementation available")
method onCommunityMuted*(self: AccessInterface, chatId: string, muted: bool) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -899,6 +899,9 @@ method onJoinedCommunity*(self: Module) =
method onMarkAllMessagesRead*(self: Module, chat: ChatDto) =
self.updateBadgeNotifications(chat, hasUnreadMessages=false, unviewedMentionsCount=0)
method onMarkMessageAsUnread*(self: Module, chat: ChatDto) =
self.updateBadgeNotifications(chat, hasUnreadMessages=true, chat.unviewedMentionsCount)
method markAllMessagesRead*(self: Module, chatId: string) =
self.controller.markAllMessagesRead(chatId)

View File

@ -784,6 +784,24 @@ QtObject:
defer: index.delete
self.dataChanged(index, index, @[ModelRole.Seen.int])
proc setMessageMarker*(self: Model, messageId: string) =
self.firstUnseenMessageId = messageId
self.resetNewMessagesMarker()
proc markMessageAsUnread*(self: Model, messageId: string) =
self.setMessageMarker(messageId)
for i in 0 ..< self.items.len:
let item = self.items[i]
if item.id == messageId and item.seen:
item.seen = false
let index = self.createIndex(i, 0, nil)
defer: index.delete
self.dataChanged(index, index, @[ModelRole.Seen.int])
break
proc markAsSeen*(self: Model, messages: seq[string]) =
var messagesSet = toHashSet(messages)

View File

@ -763,6 +763,15 @@ QtObject:
except Exception as e:
error "error while getting members", msg = e.msg, communityID, chatId
proc updateUnreadMessage*(self: Service, chatID: string, messagesCount:int, messagesWithMentionsCount:int) =
var chat = self.getChatById(chatID)
if chat.id == "":
return
chat.unviewedMessagesCount = messagesCount
chat.unviewedMentionsCount = messagesWithMentionsCount
self.updateOrAddChat(chat)
proc updateUnreadMessagesAndMentions*(self: Service, chatID: string, markAllAsRead: bool, markAsReadCount: int, markAsReadMentionsCount: int) =
var chat = self.getChatById(chatID)
if chat.id == "":

View File

@ -126,14 +126,16 @@ const asyncMarkAllMessagesReadTask: Task = proc(argEncoded: string) {.gcsafe, ni
let response = status_go.markAllMessagesFromChatWithIdAsRead(arg.chatId)
var activityCenterNotifications: JsonNode
if response.result["activityCenterNotifications"] != nil:
activityCenterNotifications = response.result["activityCenterNotifications"]
discard response.result.getProp("activityCenterNotifications", activityCenterNotifications)
let responseJson = %*{
"chatId": arg.chatId,
"activityCenterNotifications": activityCenterNotifications,
"error": response.error
}
if activityCenterNotifications != nil:
responseJson["activityCenterNotifications"] = activityCenterNotifications
arg.finish(responseJson)
#################################################
@ -157,8 +159,7 @@ const asyncMarkCertainMessagesReadTask: Task = proc(argEncoded: string) {.gcsafe
discard response.result.getProp("countWithMentions", countWithMentions)
var activityCenterNotifications: JsonNode
if response.result["activityCenterNotifications"] != nil:
activityCenterNotifications = response.result["activityCenterNotifications"]
discard response.result.getProp("activityCenterNotifications", activityCenterNotifications)
var error = ""
if(count == 0):
@ -169,9 +170,12 @@ const asyncMarkCertainMessagesReadTask: Task = proc(argEncoded: string) {.gcsafe
"messagesIds": arg.messagesIds,
"count": count,
"countWithMentions": countWithMentions,
"activityCenterNotifications": activityCenterNotifications,
"error": error
}
if activityCenterNotifications != nil:
responseJson["activityCenterNotifications"] = activityCenterNotifications
arg.finish(responseJson)
@ -295,3 +299,43 @@ const asyncGetMessageByMessageIdTask: Task = proc(argEncoded: string) {.gcsafe,
}
arg.finish(output)
#################################################
# Async mark message as unread
#################################################
type
AsyncMarkMessageAsUnreadTaskArg = ref object of QObjectTaskArg
messageId*: string
chatId*: string
const asyncMarkMessageAsUnreadTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncMarkMessageAsUnreadTaskArg](argEncoded)
var responseJson = %*{
"chatId": arg.chatId,
"messageId": arg.messageId,
"messagesCount": 0,
"messagesWithMentionsCount": 0,
"error": ""
}
try:
let response = status_go.markMessageAsUnread(arg.chatId, arg.messageId)
var activityCenterNotifications: JsonNode
discard response.result.getProp("activityCenterNotifications", activityCenterNotifications)
if activityCenterNotifications != nil:
responseJson["activityCenterNotifications"] = activityCenterNotifications
responseJson["messagesCount"] = %response.result["count"]
responseJson["messagesWithMentionsCount"] = %response.result["countWithMentions"]
if response.error != nil:
responseJson["error"] = %response.error
except Exception as e:
error "asyncMarkMessageAsUnreadTask failed", message = e.msg
responseJson["error"] = %e.msg
arg.finish(responseJson)

View File

@ -63,6 +63,7 @@ const SIGNAL_RELOAD_MESSAGES* = "reloadMessages"
const SIGNAL_URLS_UNFURLED* = "urlsUnfurled"
const SIGNAL_GET_MESSAGE_FINISHED* = "getMessageFinished"
const SIGNAL_URLS_UNFURLING_PLAN_READY* = "urlsUnfurlingPlanReady"
const SIGNAL_MESSAGE_MARKED_AS_UNREAD* = "messageMarkedAsUnread"
include async_tasks
@ -90,6 +91,12 @@ type
messageId*: string
actionInitiatedBy*: string
MessageMarkMessageAsUnreadArgs* = ref object of Args
chatId*: string
messageId*: string
messagesCount*: int
messagesWithMentionsCount*: int
MessagesMarkedAsReadArgs* = ref object of Args
chatId*: string
allMessagesMarked*: bool
@ -581,6 +588,20 @@ QtObject:
except Exception as e:
error "error: ", procName="pinUnpinMessage", errName = e.name, errDesription = e.msg
proc asyncMarkMessageAsUnread*(self: Service, chatId: string, messageId: string) =
if (chatId.len == 0):
error "empty chat id", procName="markAllMessagesRead"
return
let arg = AsyncMarkMessageAsUnreadTaskArg(
tptr: cast[ByteAddress](asyncMarkMessageAsUnreadTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncMarkMessageAsUnread",
messageId: messageId,
chatId: chatId
)
self.threadpool.start(arg)
proc getMessageByMessageId*(self: Service, messageId: string): GetMessageResult =
try:
result = GetMessageResult()
@ -723,8 +744,13 @@ QtObject:
let data = MessagesMarkedAsReadArgs(chatId: chatId, allMessagesMarked: true)
self.events.emit(SIGNAL_MESSAGES_MARKED_AS_READ, data)
self.events.emit(SIGNAL_PARSE_RAW_ACTIVITY_CENTER_NOTIFICATIONS,
RawActivityCenterNotificationsArgs(activityCenterNotifications: responseObj{"activityCenterNotifications"}))
var activityCenterNotifications: JsonNode
discard responseObj.getProp("activityCenterNotifications", activityCenterNotifications)
if activityCenterNotifications != nil:
self.events.emit(SIGNAL_PARSE_RAW_ACTIVITY_CENTER_NOTIFICATIONS,
RawActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotifications))
proc markAllMessagesRead*(self: Service, chatId: string) =
if (chatId.len == 0):
@ -740,6 +766,49 @@ QtObject:
self.threadpool.start(arg)
proc onAsyncMarkMessageAsUnread*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
if responseObj.kind != JObject:
raise newException(RpcException, "markMessageAsUnread response is not a json object")
var error: string
discard responseObj.getProp("error", error)
if error.len > 0:
error "error: ", procName="onAsyncMarkMessageAsUnread", errDescription=error
return
var chatId, messageId: string
var count, countWithMentions: int
discard responseObj.getProp("chatId", chatId)
discard responseObj.getProp("messageId", messageId)
discard responseObj.getProp("messagesCount", count)
discard responseObj.getProp("messagesWithMentionsCount", countWithMentions)
let data = MessageMarkMessageAsUnreadArgs(
chatId: chatId,
messageId: messageId,
messagesCount: count,
messagesWithMentionsCount: countWithMentions
)
self.chatService.updateUnreadMessage(chatId, count, countWithMentions)
self.events.emit(SIGNAL_MESSAGE_MARKED_AS_UNREAD, data)
var activityCenterNotifications: JsonNode
discard responseObj.getProp("activityCenterNotifications", activityCenterNotifications)
if activityCenterNotifications != nil:
self.events.emit(SIGNAL_PARSE_RAW_ACTIVITY_CENTER_NOTIFICATIONS,
RawActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotifications))
except Exception as e:
error "error: ", procName="markMessageAsUnread", errName = e.name, errDesription = e.msg
proc onMarkCertainMessagesRead*(self: Service, response: string) {.slot.} =
let responseObj = response.parseJson
@ -774,8 +843,13 @@ QtObject:
messagesCount: count,
messagesWithMentionsCount: countWithMentions)
self.events.emit(SIGNAL_MESSAGES_MARKED_AS_READ, data)
self.events.emit(SIGNAL_PARSE_RAW_ACTIVITY_CENTER_NOTIFICATIONS,
RawActivityCenterNotificationsArgs(activityCenterNotifications: responseObj{"activityCenterNotifications"}))
var activityCenterNotifications: JsonNode
discard responseObj.getProp("activityCenterNotifications", activityCenterNotifications)
if activityCenterNotifications != nil:
self.events.emit(SIGNAL_PARSE_RAW_ACTIVITY_CENTER_NOTIFICATIONS,
RawActivityCenterNotificationsArgs(activityCenterNotifications: activityCenterNotifications))
proc markCertainMessagesRead*(self: Service, chatId: string, messagesIds: seq[string]) =
if (chatId.len == 0):

View File

@ -32,6 +32,10 @@ proc pinUnpinMessage*(chatId: string, messageId: string, pin: bool): RpcResponse
}]
result = callPrivateRPC("sendPinMessage".prefix, payload)
proc markMessageAsUnread*(chatId: string, messageId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %*[chatId, messageId]
result = callPrivateRPC("markMessageAsUnread".prefix, payload)
proc getMessageByMessageId*(messageId: string): RpcResponse[JsonNode] {.raises: [Exception].} =
let payload = %* [messageId]
result = callPrivateRPC("messageByMessageID".prefix, payload)

View File

@ -352,3 +352,66 @@ suite "mark as seen":
check(model.items[0].seen == true)
check(model.items[1].seen == true)
check(model.items[2].seen == true)
suite "mark message as unread":
setup:
let model = newModel()
var msg1 = createTestMessageItem("0xa", 1)
msg1.seen = true
var msg2 = createTestMessageItem("0xb", 2)
msg2.seen = true
var msg3 = createTestMessageItem("0xc", 3)
msg3.seen = true
model.insertItemsBasedOnClock(@[msg1, msg2, msg3])
require(model.items.len == 3)
check(model.items[0].seen == true)
check(model.items[1].seen == true)
check(model.items[2].seen == true)
test "mark message as unread":
model.markMessageAsUnread("0xa")
check(model.items[2].seen == false)
model.markMessageAsUnread("0xb")
check(model.items[1].seen == false)
model.markMessageAsUnread("0xc")
check(model.items[0].seen == false)
test "mark an already unread message as unread":
model.markMessageAsUnread("0xa")
check(model.items[2].seen == false)
model.markMessageAsUnread("0xa")
check(model.items[2].seen == false)
model.markMessageAsUnread("0xb")
check(model.items[1].seen == false)
model.markMessageAsUnread("0xb")
check(model.items[1].seen == false)
model.markMessageAsUnread("0xc")
check(model.items[0].seen == false)
model.markMessageAsUnread("0xc")
check(model.items[0].seen == false)
test "mark all messages as unread":
require(model.items.len == 3)
model.markMessageAsUnread("0xa")
model.markMessageAsUnread("0xc")
model.markMessageAsUnread("0xb")
# Because new row is inserted for message marker
require(model.items.len == 4)
check(model.items[0].seen == false)
check(model.items[1].seen == false)
# message marker is inserted on top of the last inserted element
# last inserted element is `0xc` which is at position 0
# and marker is insert last at position : position('0xb') - 1 equals to position 2 here
check(model.items[2].seen == true)
check(model.items[3].seen == false)

View File

@ -19,6 +19,7 @@ QtObject {
readonly property int chatType: messageModule ? messageModule.chatType : Constants.chatType.unknown
readonly property string chatColor: messageModule ? messageModule.chatColor : Style.current.blue
readonly property string chatIcon: messageModule ? messageModule.chatIcon : ""
readonly property bool keepUnread: messageModule ? messageModule.keepUnread : false
onMessageModuleChanged: {
if(!messageModule)
@ -36,6 +37,14 @@ QtObject {
messageModule.loadMoreMessages()
}
function setKeepUnread(flag: bool) {
if (!messageModule) {
return
}
messageModule.updateKeepUnread(flag)
}
function getMessageByIdAsJson (id) {
if (!messageModule) {
console.warn("getMessageByIdAsJson: Failed to parse message, because messageModule is not set")
@ -124,6 +133,13 @@ QtObject {
messageModule.deleteMessage(messageId)
}
function markMessageAsUnread(messageId) {
if (!messageModule) {
return
}
messageModule.markMessageAsUnread(messageId)
}
function warnAndDeleteMessage(messageId) {
if (localAccountSensitiveSettings.showDeleteMessageWarning)
Global.openDeleteMessagePopup(messageId, this)

View File

@ -52,13 +52,18 @@ Item {
readonly property real scrollY: chatLogView.visibleArea.yPosition * chatLogView.contentHeight
readonly property bool isMostRecentMessageInViewport: chatLogView.visibleArea.yPosition >= 0.999 - chatLogView.visibleArea.heightRatio
readonly property var chatDetails: chatContentModule && chatContentModule.chatDetails || null
readonly property bool keepUnread: messageStore.keepUnread
readonly property var loadMoreMessagesIfScrollBelowThreshold: Backpressure.oneInTimeQueued(root, 100, function() {
if(scrollY < 1000) messageStore.loadMoreMessages()
})
function setKeepUnread(flag: bool) {
root.messageStore.setKeepUnread(flag)
}
function markAllMessagesReadIfMostRecentMessageIsInViewport() {
if (!isMostRecentMessageInViewport || !chatLogView.visible) {
if (!isMostRecentMessageInViewport || !chatLogView.visible || keepUnread) {
return
}
@ -107,6 +112,7 @@ Item {
target: !!d.chatDetails ? d.chatDetails : null
function onActiveChanged() {
d.setKeepUnread(false)
d.markAllMessagesReadIfMostRecentMessageIsInViewport()
d.loadMoreMessagesIfScrollBelowThreshold()
}

View File

@ -47,6 +47,7 @@ StatusMenu {
signal toggleReaction(string messageId, int emojiId)
signal deleteMessage(string messageId)
signal editClicked(string messageId)
signal markMessageAsUnread(string messageId)
width: Math.max(emojiContainer.visible ? emojiContainer.width : 0, 230)
@ -153,6 +154,17 @@ StatusMenu {
}
}
StatusAction {
id: markMessageAsUnreadAction
text: qsTr("Mark as unread")
icon.name: "hide"
enabled: !root.disabledForChat
onTriggered: {
root.markMessageAsUnread(root.messageId)
root.close()
}
}
StatusMenuSeparator {
visible: deleteMessageAction.enabled &&
(replyToMenuItem.enabled ||

View File

@ -980,6 +980,21 @@ Loader {
}
}
},
Loader {
active: !root.editModeOn && delegate.hovered && !delegate.hideQuickActions
visible: active
sourceComponent: StatusFlatRoundButton {
objectName: "markAsUnreadButton"
width: d.chatButtonSize
height: d.chatButtonSize
icon.name: "hide"
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Mark as unread")
onClicked: {
root.messageStore.markMessageAsUnread(root.messageId)
}
}
},
Loader {
active: {
if(!delegate.hovered)
@ -1102,6 +1117,10 @@ Loader {
root.chatId)
}
onMarkMessageAsUnread: (messageId) => {
root.messageStore.markMessageAsUnread(messageId)
}
onToggleReaction: (messageId, emojiId) => {
root.messageStore.toggleReaction(messageId, emojiId)
}

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit fe604b2806dfa51ea748c5e61c2a87cc7ffd9c4a
Subproject commit 271778a1e07e585a12790b4e2226f13e36ea89f4