feat(desktop/communities): View community member messages functionality (#14002)

* feat(desktop/communities): View member messages functionality
This commit is contained in:
Mykhailo Prakhov 2024-03-20 11:50:10 +01:00 committed by GitHub
parent a48b2532ae
commit a586c6d352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 580 additions and 30 deletions

View File

@ -100,6 +100,7 @@ proc createMessageItemFromDto(self: Module, message: MessageDto, communityId: st
return msg_item_qobj.newMessageItem(msg_item.initItem(
message.id,
communityId, # we don't received community id via `activityCenterNotifications` api call
message.chatId,
message.responseTo,
message.`from`,
contactDetails.defaultDisplayName,

View File

@ -142,3 +142,6 @@ method setPermissionsCheckOngoing*(self: AccessInterface, value: bool) {.base.}
method getPermissionsCheckOngoing*(self: AccessInterface): bool {.base.} =
raise newException(ValueError, "No implementation available")
method scrollToMessage*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -151,6 +151,7 @@ proc createMessageItemsFromMessageDtos(self: Module, messages: seq[MessageDto],
var item = initItem(
message.id,
message.chatId,
message.communityId,
message.responseTo,
message.`from`,
@ -239,6 +240,7 @@ proc createFetchMoreMessagesItem(self: Module): Item =
result = initItem(
FETCH_MORE_MESSAGES_MESSAGE_ID,
communityId = "",
chatId = "",
responseToMessageWithId = "",
senderId = chatDto.id,
senderDisplayName = "",
@ -306,6 +308,7 @@ proc createChatIdentifierItem(self: Module): Item =
result = initItem(
CHAT_IDENTIFIER_MESSAGE_ID,
communityId = "",
chatId = "",
responseToMessageWithId = "",
senderId = chatDto.id,
senderDisplayName = chatName,
@ -389,7 +392,7 @@ proc currentUserWalletContainsAddress(self: Module, address: string): bool =
return false
method reevaluateViewLoadingState*(self: Module) =
let loading = not self.initialMessagesLoaded or
let loading = not self.initialMessagesLoaded or
not self.firstUnseenMessageState.initialized or
self.firstUnseenMessageState.fetching or
self.view.getMessageSearchOngoing()

View File

@ -58,7 +58,7 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
messageService)
result.moduleLoaded = false
result.inputAreaModule = input_area_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
result.inputAreaModule = input_area_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
chatService, communityService, contactService, gifService, messageService, settingsService)
result.messagesModule = messages_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
contactService, communityService, chatService, messageService, mailserversService, sharedUrlsService)
@ -174,6 +174,7 @@ proc buildPinnedMessageItem(self: Module, message: MessageDto, actionInitiatedBy
item = pinned_msg_item.initItem(
message.id,
message.communityId,
message.chatId,
message.responseTo,
message.`from`,
contactDetails.defaultDisplayName,
@ -411,7 +412,7 @@ method amIChatAdmin*(self: Module): bool =
return false
else:
let communityDto = self.controller.getCommunityDetails()
return communityDto.memberRole == MemberRole.Owner or
return communityDto.memberRole == MemberRole.Owner or
communityDto.memberRole == MemberRole.Admin or communityDto.memberRole == MemberRole.TokenMaster
method onUpdateViewOnlyPermissionsSatisfied*(self: Module, value: bool) =
@ -425,3 +426,6 @@ method setPermissionsCheckOngoing*(self: Module, value: bool) =
method getPermissionsCheckOngoing*(self: Module): bool =
self.view.getPermissionsCheckOngoing()
method scrollToMessage*(self: Module, messageId: string) =
self.messagesModule.scrollToMessage(messageId)

View File

@ -397,6 +397,20 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_MAILSERVER_HISTORY_REQUEST_COMPLETED) do(e:Args):
self.delegate.setLoadingHistoryMessagesInProgress(false)
self.events.on(SIGNAL_COMMUNITY_MEMBER_ALL_MESSAGES) do(e:Args):
var args = CommunityMemberMessagesArgs(e)
if args.communityId == self.sectionId:
self.delegate.onCommunityMemberMessagesLoaded(args.messages)
self.events.on(SIGNAL_MESSAGES_DELETED) do(e:Args):
var args = MessagesDeletedArgs(e)
let isSectionEmpty = args.communityId == ""
if args.communityId == self.sectionId or isSectionEmpty:
for chatId, messagesIds in args.deletedMessages:
if isSectionEmpty and not self.delegate.communityContainsChat(chatId):
continue
self.delegate.onCommunityMemberMessagesDeleted(messagesIds)
proc isCommunity*(self: Controller): bool =
return self.isCommunitySection
@ -725,3 +739,14 @@ proc waitingOnNewCommunityOwnerToConfirmRequestToRejoin*(self: Controller, commu
proc setCommunityShard*(self: Controller, shardIndex: int) =
self.communityService.asyncSetCommunityShard(self.getMySectionId(), shardIndex)
proc loadCommunityMemberMessages*(self: Controller, communityId: string, memberPubKey: string) =
self.messageService.asyncLoadCommunityMemberAllMessages(communityId, memberPubKey)
proc getTransactionDetails*(self: Controller, message: MessageDto): (string,string) =
return self.messageService.getTransactionDetails(message)
proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] =
return self.messageService.getWalletAccounts()
proc deleteCommunityMemberMessages*(self: Controller, memberPubKey: string, messageId: string, chatId: string) =
self.messageService.deleteCommunityMemberMessages(self.getMySectionId(), memberPubKey, messageId, chatId)

View File

@ -404,3 +404,20 @@ method setCommunityShard*(self: AccessInterface, shardIndex: int) {.base.} =
method setShardingInProgress*(self: AccessInterface, value: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method loadCommunityMemberMessages*(self: AccessInterface, communityId: string, memberPubKey: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onCommunityMemberMessagesLoaded*(self: AccessInterface, messages: seq[MessageDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method deleteCommunityMemberMessages*(self: AccessInterface, memberPubKey: string, messageId: string, chatId: string) {.base.} =
raise newException(ValueError, "No implementation available")
method onCommunityMemberMessagesDeleted*(self: AccessInterface, messages: seq[string]) {.base.} =
raise newException(ValueError, "No implementation available")
method communityContainsChat*(self: AccessInterface, chatId: string): bool {.base.} =
raise newException(ValueError, "No implementation available")
method openCommunityChatAndScrollToMessage*(self: AccessInterface, chatId: string, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")

View File

@ -5,6 +5,9 @@ import ../io_interface as delegate_interface
import view, controller, active_item
import model as chats_model
import item as chat_item
import ../../shared_models/message_item as member_msg_item
import ../../shared_models/message_model as member_msg_model
import ../../shared_models/message_transaction_parameters_item
import ../../shared_models/user_item as user_item
import ../../shared_models/user_model as user_model
import ../../shared_models/token_permissions_model
@ -87,7 +90,7 @@ proc addOrUpdateChat(self: Module,
sharedUrlsService: shared_urls_service.Service,
setChatAsActive: bool = true,
insertIntoModel: bool = true,
): Item
): chat_item.Item
proc newModule*(
delegate: delegate_interface.AccessInterface,
@ -120,6 +123,95 @@ proc newModule*(
result.chatContentModules = initOrderedTable[string, chat_content_module.AccessInterface]()
proc currentUserWalletContainsAddress(self: Module, address: string): bool =
if (address.len == 0):
return false
let accounts = self.controller.getWalletAccounts()
for acc in accounts:
if (acc.address == address):
return true
return false
# TODO: duplicates in chats and messages
proc buildCommunityMemberMessageItem(self: Module, message: MessageDto): member_msg_item.Item =
let contactDetails = self.controller.getContactDetails(message.`from`)
let communityChats = self.controller.getAllChats(self.getMySectionId())
var quotedMessageAuthorDetails = ContactDetails()
if message.quotedMessage.`from` != "":
if message.`from` == message.quotedMessage.`from`:
quotedMessageAuthorDetails = contactDetails
else:
quotedMessageAuthorDetails = self.controller.getContactDetails(message.quotedMessage.`from`)
var transactionContract = message.transactionParameters.contract
var transactionValue = message.transactionParameters.value
var isCurrentUser = contactDetails.isCurrentUser
if message.contentType == ContentType.Transaction:
(transactionContract, transactionValue) = self.controller.getTransactionDetails(message)
if message.transactionParameters.fromAddress != "":
isCurrentUser = self.currentUserWalletContainsAddress(message.transactionParameters.fromAddress)
return member_msg_item.initItem(
message.id,
message.communityId,
message.chatId,
message.responseTo,
message.`from`,
contactDetails.defaultDisplayName,
contactDetails.optionalName,
contactDetails.icon,
contactDetails.colorHash,
isCurrentUser,
contactDetails.dto.added,
message.outgoingStatus,
self.controller.getRenderedText(message.parsedText, communityChats),
message.text,
message.parsedText,
message.image,
message.containsContactMentions(),
message.seen,
timestamp = message.timestamp,
clock = message.clock,
message.contentType,
message.messageType,
message.contactRequestState,
message.sticker.url,
message.sticker.pack,
message.links,
message.linkPreviews,
newTransactionParametersItem(message.transactionParameters.id,
message.transactionParameters.fromAddress,
message.transactionParameters.address,
transactionContract,
transactionValue,
message.transactionParameters.transactionHash,
message.transactionParameters.commandState,
message.transactionParameters.signature),
message.mentionedUsersPks,
contactDetails.dto.trustStatus,
contactDetails.dto.ensVerified,
message.discordMessage,
resendError = "",
message.deleted,
message.deletedBy,
deletedByContactDetails = ContactDetails(),
message.mentioned,
message.quotedMessage.`from`,
message.quotedMessage.text,
self.controller.getRenderedText(message.quotedMessage.parsedText, communityChats),
message.quotedMessage.contentType,
message.quotedMessage.deleted,
message.quotedMessage.discordMessage,
quotedMessageAuthorDetails,
message.quotedMessage.albumImages,
message.quotedMessage.albumImagesCount,
message.albumId,
if len(message.albumId) == 0: @[] else: @[message.image],
if len(message.albumId) == 0: @[] else: @[message.id],
message.albumImagesCount,
message.bridgeMessage,
message.quotedMessage.bridgeMessage,
)
method delete*(self: Module) =
self.controller.delete
self.view.delete
@ -166,7 +258,7 @@ proc removeSubmodule(self: Module, chatId: string) =
self.chatContentModules.del(chatId)
proc addCategoryItem(self: Module, category: Category, memberRole: MemberRole, communityId: string, insertIntoModel: bool = true): Item =
proc addCategoryItem(self: Module, category: Category, memberRole: MemberRole, communityId: string, insertIntoModel: bool = true): chat_item.Item =
let hasUnreadMessages = self.controller.chatsWithCategoryHaveUnreadMessages(communityId, category.id)
result = chat_item.initItem(
id = category.id,
@ -210,7 +302,7 @@ proc buildChatSectionUI(
var selectedItemId = ""
let sectionLastOpenChat = singletonInstance.localAccountSensitiveSettings.getSectionLastOpenChat(self.controller.getMySectionId())
var items: seq[Item] = @[]
var items: seq[chat_item.Item] = @[]
for categoryDto in channelGroup.categories:
# Add items for the categories. We use a special type to identify categories
items.add(self.addCategoryItem(categoryDto, channelGroup.memberRole, channelGroup.id))
@ -544,7 +636,7 @@ proc addNewChat(
sharedUrlsService: shared_urls_service.Service,
setChatAsActive: bool = true,
insertIntoModel: bool = true,
): Item =
): chat_item.Item =
let hasNotification =chatDto.unviewedMessagesCount > 0
let notificationsCount = chatDto.unviewedMentionsCount
@ -1262,7 +1354,7 @@ proc addOrUpdateChat(self: Module,
sharedUrlsService: shared_urls_service.Service,
setChatAsActive: bool = true,
insertIntoModel: bool = true,
): Item =
): chat_item.Item =
let sectionId = self.controller.getMySectionId()
if(belongsToCommunity and sectionId != chat.communityId or
@ -1326,7 +1418,7 @@ method addOrUpdateChat*(self: Module,
sharedUrlsService: shared_urls_service.Service,
setChatAsActive: bool = true,
insertIntoModel: bool = true,
): Item =
): chat_item.Item =
result = self.addOrUpdateChat(
chat,
ChannelGroupDto(),
@ -1430,3 +1522,33 @@ method setCommunityShard*(self: Module, shardIndex: int) =
method setShardingInProgress*(self: Module, value: bool) =
self.view.setShardingInProgress(value)
method loadCommunityMemberMessages*(self: Module, communityId: string, memberPubKey: string) =
self.view.getMemberMessagesModel().clear()
self.controller.loadCommunityMemberMessages(communityId, memberPubKey)
method onCommunityMemberMessagesLoaded*(self: Module, messages: seq[MessageDto]) =
var viewItems: seq[member_msg_item.Item]
for message in messages:
let item = self.buildCommunityMemberMessageItem(message)
viewItems.add(item)
if viewItems.len == 0:
return
self.view.getMemberMessagesModel().insertItemsBasedOnClock(viewItems)
method deleteCommunityMemberMessages*(self: Module, memberPubKey: string, messageId: string, chatId: string) =
self.controller.deleteCommunityMemberMessages(memberPubKey, messageId, chatId)
method onCommunityMemberMessagesDeleted*(self: Module, deletedMessages: seq[string]) =
if self.view.getMemberMessagesModel().rowCount > 0:
for deletedMessageId in deletedMessages:
self.view.getMemberMessagesModel().removeItem(deletedMessageId)
method communityContainsChat*(self: Module, chatId: string): bool =
return self.chatContentModules.hasKey(chatId)
method openCommunityChatAndScrollToMessage*(self: Module, chatId: string, messageId: string) =
self.delegate.setActiveSectionById(self.getMySectionId())
self.setActiveItem(chatId)
self.chatContentModules[chatId].scrollToMessage(messageId)

View File

@ -2,6 +2,7 @@ import NimQml, json, sequtils, strutils
import model as chats_model
import item, active_item
import ../../shared_models/user_model as user_model
import ../../shared_models/message_model as member_msg_model
import ../../shared_models/token_permissions_model
import io_interface
@ -32,6 +33,9 @@ QtObject:
isWaitingOnNewCommunityOwnerToConfirmRequestToRejoin: bool
shardingInProgress: bool
allChannelsAreHiddenBecauseNotPermitted: bool
memberMessagesModel: member_msg_model.Model
memberMessagesModelVariant: QVariant
proc delete*(self: View) =
self.model.delete
@ -46,6 +50,8 @@ QtObject:
self.editCategoryChannelsVariant.delete
self.tokenPermissionsModel.delete
self.tokenPermissionsVariant.delete
self.memberMessagesModel.delete
self.memberMessagesModelVariant.delete
self.QObject.delete
@ -71,6 +77,8 @@ QtObject:
result.chatsLoaded = false
result.communityMetrics = "[]"
result.isWaitingOnNewCommunityOwnerToConfirmRequestToRejoin = false
result.memberMessagesModel = member_msg_model.newModel()
result.memberMessagesModelVariant = newQVariant(result.memberMessagesModel)
proc load*(self: View) =
self.delegate.viewDidLoad()
@ -508,4 +516,21 @@ QtObject:
if (allAreHidden == self.allChannelsAreHiddenBecauseNotPermitted):
return
self.allChannelsAreHiddenBecauseNotPermitted = allAreHidden
self.allChannelsAreHiddenBecauseNotPermittedChanged()
self.allChannelsAreHiddenBecauseNotPermittedChanged()
proc getMemberMessagesModel*(self: View): member_msg_model.Model =
return self.memberMessagesModel
proc getMemberMessagesModelVariant(self: View): QVariant {.slot.} =
return self.memberMessagesModelVariant
QtProperty[QVariant] memberMessagesModel:
read = getMemberMessagesModelVariant
proc loadCommunityMemberMessages*(self: View, communityId: string, memberPubKey: string) {.slot.} =
self.delegate.loadCommunityMemberMessages(communityId, memberPubKey)
proc deleteCommunityMemberMessages*(self: View, memberPubKey: string, messageId: string, chatId: string) {.slot.} =
self.delegate.deleteCommunityMemberMessages(memberPubKey, messageId, chatId)
proc openCommunityChatAndScrollToMessage*(self: View, chatId: string, messageId: string) {.slot.} =
self.delegate.openCommunityChatAndScrollToMessage(chatId, messageId)

View File

@ -13,6 +13,7 @@ type
Item* = ref object
id: string
communityId: string
chatId: string
responseToMessageWithId: string
senderId: string
senderDisplayName: string
@ -75,6 +76,7 @@ type
proc initItem*(
id,
communityId,
chatId,
responseToMessageWithId,
senderId,
senderDisplayName,
@ -128,6 +130,7 @@ proc initItem*(
result = Item()
result.id = id
result.communityId = communityId
result.chatId = chatId
result.responseToMessageWithId = responseToMessageWithId
result.senderId = senderId
result.senderDisplayName = senderDisplayName
@ -228,6 +231,7 @@ proc initNewMessagesMarkerItem*(clock, timestamp: int64): Item =
return initItem(
id = "",
communityId = "",
chatId = "",
responseToMessageWithId = "",
senderId = "",
senderDisplayName = "",
@ -283,6 +287,7 @@ proc `$`*(self: Item): string =
result = fmt"""Item(
id: {$self.id},
communityId: {$self.communityId},
chatId: {$self.chatId},
responseToMessageWithId: {self.responseToMessageWithId},
senderId: {self.senderId},
senderDisplayName: {$self.senderDisplayName},
@ -320,6 +325,9 @@ proc id*(self: Item): string {.inline.} =
proc communityId*(self: Item): string {.inline.} =
self.communityId
proc chatId*(self: Item): string {.inline.} =
self.chatId
proc responseToMessageWithId*(self: Item): string {.inline.} =
self.responseToMessageWithId
@ -419,7 +427,7 @@ proc albumMessageIds*(self: Item): seq[string] {.inline.} =
proc `albumMessageIds=`*(self: Item, value: seq[string]) {.inline.} =
self.albumMessageIds = value
proc albumImagesCount*(self: Item): int {.inline.} =
proc albumImagesCount*(self: Item): int {.inline.} =
self.albumImagesCount
proc bridgeName*(self: Item): string {.inline.} =
@ -516,6 +524,7 @@ proc toJsonNode*(self: Item): JsonNode =
result = %* {
"id": self.id,
"communityId": self.communityId,
"chatId": self.chatId,
"responseToMessageWithId": self.responseToMessageWithId,
"senderId": self.senderId,
"senderDisplayName": self.senderDisplayName,
@ -661,8 +670,8 @@ proc quotedMessageAlbumMessageImages*(self: Item): seq[string] {.inline.} =
proc `quotedMessageAlbumMessageImages=`*(self: Item, value: seq[string]) {.inline.} =
self.quotedMessageAlbumMessageImages = value
proc quotedMessageAlbumImagesCount*(self: Item): int {.inline.} =
proc quotedMessageAlbumImagesCount*(self: Item): int {.inline.} =
self.quotedMessageAlbumImagesCount
proc `quotedMessageAlbumImagesCount=`*(self: Item, value: int) {.inline.} =
proc `quotedMessageAlbumImagesCount=`*(self: Item, value: int) {.inline.} =
self.quotedMessageAlbumImagesCount = value

View File

@ -16,6 +16,7 @@ type
NextMsgIndex
NextMsgTimestamp
CommunityId
ChatId
ResponseToMessageWithId
SenderId
SenderDisplayName
@ -123,6 +124,7 @@ QtObject:
ModelRole.NextMsgIndex.int:"nextMsgIndex",
ModelRole.NextMsgTimestamp.int:"nextMsgTimestamp",
ModelRole.CommunityId.int:"communityId",
ModelRole.ChatId.int:"chatId",
ModelRole.ResponseToMessageWithId.int:"responseToMessageWithId",
ModelRole.SenderId.int:"senderId",
ModelRole.SenderDisplayName.int:"senderDisplayName",
@ -231,6 +233,8 @@ QtObject:
result = newQVariant(0)
of ModelRole.CommunityId:
result = newQVariant(item.communityId)
of ModelRole.ChatId:
result = newQVariant(item.chatId)
of ModelRole.ResponseToMessageWithId:
result = newQVariant(item.responseToMessageWithId)
of ModelRole.SenderId:

View File

@ -186,7 +186,7 @@ type
const asyncGetFirstUnseenMessageIdForTaskArg: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncGetFirstUnseenMessageIdForTaskArg](argEncoded)
let responseJson = %*{
"messageId": "",
"chatId": arg.chatId,
@ -338,3 +338,34 @@ const asyncMarkMessageAsUnreadTask: Task = proc(argEncoded: string) {.gcsafe, ni
responseJson["error"] = %e.msg
arg.finish(responseJson)
#################################################
# Async load community member messages
#################################################
type
AsyncLoadCommunityMemberAllMessagesTaskArg = ref object of QObjectTaskArg
communityId*: string
memberPubKey*: string
const asyncLoadCommunityMemberAllMessagesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncLoadCommunityMemberAllMessagesTaskArg](argEncoded)
var responseJson = %*{
"communityId": arg.communityId,
"error": ""
}
try:
let response = status_go.getCommunityMemberAllMessages(arg.communityId, arg.memberPubKey)
if not response.error.isNil:
raise newException(CatchableError, "response error: " & response.error.message)
responseJson["messages"] = response.result
except Exception as e:
error "error: ", procName = "asyncLoadCommunityMemberAllMessagesTask", errName = e.name,
errDesription = e.msg, communityId=arg.communityId, memberPubKey=arg.memberPubKey
responseJson["error"] = %e.msg
arg.finish(responseJson)

View File

@ -211,7 +211,7 @@ proc toQuotedMessage*(jsonObj: JsonNode): QuotedMessage =
var bridgeMessageObj: JsonNode
if(jsonObj.getProp("bridgeMessage", bridgeMessageObj)):
result.bridgeMessage = toBridgeMessage(bridgeMessageObj)
var quotedImagesArr: JsonNode
if jsonObj.getProp("albumImages", quotedImagesArr):
for element in quotedImagesArr.getElems():
@ -313,7 +313,7 @@ proc toMessageDto*(jsonObj: JsonNode): MessageDto =
if jsonObj.getProp("statusLinkPreviews", statusLinkPreviewsArr):
for element in statusLinkPreviewsArr.getElems():
result.linkPreviews.add(element.toLinkPreview(false))
var parsedTextArr: JsonNode
if(jsonObj.getProp("parsedText", parsedTextArr) and parsedTextArr.kind == JArray):
for pTextObj in parsedTextArr:

View File

@ -64,6 +64,7 @@ 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"
const SIGNAL_COMMUNITY_MEMBER_ALL_MESSAGES* = "communityMemberAllMessages"
include async_tasks
@ -117,6 +118,7 @@ type
deletedBy*: string
MessagesDeletedArgs* = ref object of Args
communityId*: string
deletedMessages*: Table[string, seq[string]]
MessageDeliveredArgs* = ref object of Args
@ -154,6 +156,10 @@ type
message*: MessageDto
error*: string
CommunityMemberMessagesArgs* = ref object of Args
communityId*: string
messages*: seq[MessageDto]
QtObject:
type Service* = ref object of QObject
events: EventEmitter
@ -274,6 +280,17 @@ QtObject:
discard self.asyncLoadMoreMessagesForChat(chatId)
proc asyncLoadCommunityMemberAllMessages*(self: Service, communityId: string, memberPublicKey: string) =
let arg = AsyncLoadCommunityMemberAllMessagesTaskArg(
communityId: communityId,
memberPubKey: memberPublicKey,
tptr: cast[ByteAddress](asyncLoadCommunityMemberAllMessagesTask),
vptr: cast[ByteAddress](self.vptr),
slot: "onAsyncLoadCommunityMemberAllMessages"
)
self.threadpool.start(arg)
proc handleMessagesUpdate(self: Service, chats: var seq[ChatDto], messages: var seq[MessageDto]) =
# We included `chats` in this condition cause that's the form how `status-go` sends updates.
# The first element from the `receivedData.chats` array contains details about the chat a messages received in
@ -358,8 +375,8 @@ QtObject:
let data = MessageRemovedArgs(chatId: rm.chatId, messageId: rm.messageId, deletedBy: rm.deletedBy)
self.events.emit(SIGNAL_MESSAGE_REMOVED, data)
proc handleDeletedMessagesUpdate(self: Service, deletedMessages: Table[string, seq[string]]) =
let data = MessagesDeletedArgs(deletedMessages: deletedMessages)
proc handleDeletedMessagesUpdate(self: Service, deletedMessages: Table[string, seq[string]], communityId: string) =
let data = MessagesDeletedArgs(deletedMessages: deletedMessages, communityId: communityId)
self.events.emit(SIGNAL_MESSAGES_DELETED, data)
proc handleEmojiReactionsUpdate(self: Service, emojiReactions: seq[ReactionDto]) =
@ -430,7 +447,7 @@ QtObject:
self.handleRemovedMessagesUpdate(receivedData.removedMessages)
# Handling deleted messages updates
if (receivedData.deletedMessages.len > 0):
self.handleDeletedMessagesUpdate(receivedData.deletedMessages)
self.handleDeletedMessagesUpdate(receivedData.deletedMessages, "")
# Handling emoji reactions updates
if (receivedData.emojiReactions.len > 0):
self.handleEmojiReactionsUpdate(receivedData.emojiReactions)
@ -542,6 +559,29 @@ QtObject:
self.events.emit(SIGNAL_MESSAGES_LOADED, data)
proc onAsyncLoadCommunityMemberAllMessages*(self: Service, response: string) {.slot.} =
try:
let rpcResponseObj = response.parseJson
if rpcResponseObj{"error"}.kind != JNull and rpcResponseObj{"error"}.getStr != "":
raise newException(RpcException, rpcResponseObj{"error"}.getStr)
if rpcResponseObj{"messages"}.kind == JNull:
return
if rpcResponseObj{"messages"}.kind != JArray:
raise newException(RpcException, "invalid messages type in response")
var communityId: string
discard rpcResponseObj.getProp("communityId", communityId)
var messages = map(rpcResponseObj{"messages"}.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto())
if messages.len > 0:
self.bulkReplacePubKeysWithDisplayNames(messages)
let data = CommunityMemberMessagesArgs(communityId: communityId, messages: messages)
self.events.emit(SIGNAL_COMMUNITY_MEMBER_ALL_MESSAGES, data)
except Exception as e:
error "error: ", procName="onAsyncLoadCommunityMemberAllMessages", errName = e.name, errDesription = e.msg
proc addReaction*(self: Service, chatId: string, messageId: string, emojiId: int) =
try:
let response = status_go.addReaction(chatId, messageId, emojiId)
@ -1147,3 +1187,26 @@ proc resendChatMessage*(self: Service, messageId: string): string =
except Exception as e:
error "error: ", procName="resendChatMessage", errName = e.name, errDesription = e.msg
return fmt"{e.name}: {e.msg}"
# TODO: would be nice to make it async but in this case we will need to show come spinner in the UI during deleting the message
proc deleteCommunityMemberMessages*(self: Service, communityId: string, memberPubKey: string, messageId: string, chatId: string) =
try:
let response = status_go.deleteCommunityMemberMessages(communityId, memberPubKey, messageId, chatId)
if response.result.contains("error"):
let errMsg = response.result["error"].getStr
raise newException(RpcException, "Error deleting community member messages: " & errMsg)
var deletedMessages = initTable[string, seq[string]]()
if response.result.contains("deletedMessages"):
let deletedMessagesObj = response.result["deletedMessages"]
for chatId, messageIdsArrayJson in deletedMessagesObj:
if not deletedMessages.hasKey(chatId):
deletedMessages[chatId] = @[]
for messageId in messageIdsArrayJson:
deletedMessages[chatId].add(messageId.getStr())
self.handleDeletedMessagesUpdate(deletedMessages, communityId)
except Exception as e:
error "error: ", procName="deleteCommunityMemberMessages", errName = e.name, errDesription = e.msg

View File

@ -87,3 +87,24 @@ proc getTextURLsToUnfurl*(text: string): RpcResponse[JsonNode] =
proc unfurlUrls*(urls: seq[string]): RpcResponse[JsonNode] =
let payload = %*[urls]
result = callPrivateRPC("unfurlURLs".prefix, payload)
proc getCommunityMemberAllMessages*(communityId: string, memberPublicKey: string): RpcResponse[JsonNode] =
let payload = %* [{"communityId": communityId, "memberPublicKey": memberPublicKey}]
result = callPrivateRPC("getCommunityMemberAllMessages".prefix, payload)
proc deleteCommunityMemberMessages*(communityId: string, memberPubKey: string, messageId: string, chatId: string): RpcResponse[JsonNode] =
var messages: seq[JsonNode] = @[]
if messageId != "" and chatId != "":
messages.add(%*{
"id": messageId,
"chat_id": chatId,
})
let payload = %* [{
"communityId": communityId,
"memberPubKey": memberPubKey,
"messages": messages,
"deleteAll": messages.len() == 0
}]
result = callPrivateRPC("deleteCommunityMemberMessages".prefix, payload)

View File

@ -329,6 +329,10 @@ QtObject {
chatCommunitySectionModule.removeUserFromCommunity(pubKey);
}
function loadCommunityMemberMessages(communityId, pubKey) {
chatCommunitySectionModule.loadCommunityMemberMessages(communityId, pubKey);
}
function banUserFromCommunity(pubKey, deleteAllMessages) {
chatCommunitySectionModule.banUserFromCommunity(pubKey, deleteAllMessages);
}

View File

@ -26,6 +26,7 @@ SettingsPage {
signal unbanUserClicked(string id)
signal acceptRequestToJoin(string id)
signal declineRequestToJoin(string id)
signal viewMemberMessagesClicked(string pubKey, string displayName)
function goTo(tab: int) {
if(root.contentItem) {
@ -127,6 +128,8 @@ SettingsPage {
kickBanPopup.userId = id
kickBanPopup.open()
}
onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
MembersTabPanel {
@ -182,6 +185,7 @@ SettingsPage {
Layout.fillHeight: true
onUnbanUserClicked: root.unbanUserClicked(id)
onViewMemberMessagesClicked: root.viewMemberMessagesClicked(pubKey, displayName)
}
}
}

View File

@ -29,6 +29,7 @@ Item {
signal kickUserClicked(string id, string name)
signal banUserClicked(string id, string name)
signal unbanUserClicked(string id)
signal viewMemberMessagesClicked(string pubKey, string displayName)
signal acceptRequestToJoin(string id)
signal declineRequestToJoin(string id)
@ -94,6 +95,10 @@ Item {
readonly property bool tabIsShowingRejectButton: root.panelType === MembersTabPanel.TabType.PendingRequests
readonly property bool tabIsShowingAcceptButton: root.panelType === MembersTabPanel.TabType.PendingRequests ||
root.panelType === MembersTabPanel.TabType.DeclinedRequests
readonly property bool tabIsShowingViewMessagesButton: model.membershipRequestState !== Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete &&
(root.panelType === MembersTabPanel.TabType.AllMembers ||
root.panelType === MembersTabPanel.TabType.BannedMembers)
// Request states
readonly property bool isPending: model.membershipRequestState === Constants.CommunityMembershipRequestState.Pending
@ -130,6 +135,7 @@ Item {
}
}
readonly property bool showOnHover: isHovered && ctaAllowed
readonly property bool canDeleteMessages: itsMe || model.memberRole !== Constants.memberRole.owner
/// Button visibility ///
readonly property bool acceptButtonVisible: tabIsShowingAcceptButton && (isPending || isRejected || isRejectedPending || isAcceptedPending) && showOnHover
@ -141,6 +147,9 @@ Item {
readonly property bool kickPendingButtonVisible: tabIsShowingKickBanButtons && isKickPending
readonly property bool banPendingButtonVisible: tabIsShowingKickBanButtons && isBanPending
readonly property bool unbanButtonVisible: tabIsShowingUnbanButton && isBanned && showOnHover
readonly property bool viewMessagesButtonVisible: tabIsShowingViewMessagesButton && showOnHover
readonly property bool messagesDeletedTextVisible: showOnHover &&
model.membershipRequestState === Constants.CommunityMembershipRequestState.BannedWithAllMessagesDelete
/// Pending states ///
readonly property bool isPendingState: isAcceptedPending || isRejectedPending || isBanPending || isUnbanPending || isKickPending
@ -179,6 +188,24 @@ Item {
enabled: pendingText.visible
}
},
StatusBaseText {
text: qsTr("Messages deleted")
color: Theme.palette.baseColor1
anchors.verticalCenter: parent.verticalCenter
visible: messagesDeletedTextVisible
},
StatusButton {
id: viewMessages
anchors.verticalCenter: parent.verticalCenter
objectName: "MemberListItem_ViewMessages"
text: qsTr("View Messages")
visible: viewMessagesButtonVisible
size: StatusBaseButton.Size.Small
onClicked: root.viewMemberMessagesClicked(model.pubKey, model.displayName)
},
StatusButton {
id: kickButton
anchors.verticalCenter: parent.verticalCenter
@ -204,6 +231,8 @@ Item {
anchors.verticalCenter: parent.verticalCenter
visible: unbanButtonVisible
text: qsTr("Unban")
type: StatusBaseButton.Type.Danger
size: StatusBaseButton.Size.Small
onClicked: root.unbanUserClicked(model.pubKey)
},

View File

@ -0,0 +1,143 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick.Layouts 1.14
import QtQml.Models 2.14
import QtGraphicalEffects 1.14
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups 0.1
import StatusQ.Popups.Dialog 0.1
import utils 1.0
import shared.views.chat 1.0
StatusDialog {
id: root
property var store
property var chatCommunitySectionModule
property var memberMessagesModel: chatCommunitySectionModule.memberMessagesModel
property string memberPubKey: ""
property string displayName: ""
width: 800
title: qsTr("%1 messages").arg(root.displayName)
subtitle: qsTr("%n message(s)", "", root.memberMessagesModel.count)
ColumnLayout {
anchors.fill: parent
id: column
StatusBaseText {
visible: communityMemberMessageListView.count === 0
text: qsTr("No messages")
Layout.alignment: Qt.AlignCenter
verticalAlignment: Text.AlignVCenter
color: Style.current.secondaryText
Layout.topMargin: 40
Layout.bottomMargin: 40
}
StatusListView {
id: communityMemberMessageListView
model: root.memberMessagesModel
Layout.fillWidth: true
Layout.fillHeight: count
implicitHeight: contentHeight
delegate: Item {
id: messageDelegate
width: ListView.view.width
height: messageItem.height
MessageView {
id: messageItem
width: parent.width
rootStore: root.store
chatCommunitySectionModule: root.chatCommunitySectionModule
messageStore: root.memberMessagesModel
messageId: model.id
chatId: model.chatId
responseToMessageWithId: model.responseToMessageWithId
amIChatAdmin: true
senderId: model.senderId
senderDisplayName: model.senderDisplayName
senderOptionalName: model.senderOptionalName
senderIsEnsVerified: model.senderEnsVerified
senderIsAdded: model.senderIsAdded
senderIcon: model.senderIcon
senderColorHash: model.senderColorHash
senderTrustStatus: model.senderTrustStatus
amISender: model.amISender
messageText: model.messageText
messageImage: model.messageImage
messageTimestamp: model.timestamp
messageOutgoingStatus: model.outgoingStatus
messageContentType: model.contentType
pinnedMessage: model.pinned
messagePinnedBy: model.pinnedBy
sticker: model.sticker
stickerPack: model.stickerPack
linkPreviewModel: model.linkPreviewModel
links: model.links
transactionParams: model.transactionParameters
quotedMessageText: model.quotedMessageParsedText
quotedMessageFrom: model.quotedMessageFrom
quotedMessageContentType: model.quotedMessageContentType
quotedMessageDeleted: model.quotedMessageDeleted
quotedMessageAuthorDetailsName: model.quotedMessageAuthorName
quotedMessageAuthorDetailsDisplayName: model.quotedMessageAuthorDisplayName
quotedMessageAuthorDetailsThumbnailImage: model.quotedMessageAuthorThumbnailImage
quotedMessageAuthorDetailsEnsVerified: model.quotedMessageAuthorEnsVerified
quotedMessageAuthorDetailsIsContact: model.quotedMessageAuthorIsContact
quotedMessageAuthorDetailsColorHash: model.quotedMessageAuthorColorHash
bridgeName: model.bridgeName
isViewMemberMessagesePopup: true
shouldRepeatHeader: true
}
MouseArea {
id: mouseArea
acceptedButtons: Qt.LeftButton
cursorShape: Qt.PointingHandCursor
anchors.fill: parent
onClicked: {
root.chatCommunitySectionModule.openCommunityChatAndScrollToMessage(model.chatId, model.id)
Global.switchToCommunityChannelsView(root.chatCommunitySectionModule.getMySectionId())
root.chatCommunitySectionModule.openCommunityChatAndScrollToMessage(model.chatId, model.id)
root.closed()
}
}
}
}
}
footer: StatusDialogFooter {
id: footer
rightButtons: ObjectModel {
StatusFlatButton {
text: qsTr("Delete all messages by %1").arg(root.displayName)
enabled: communityMemberMessageListView.count > 0
type: StatusBaseButton.Type.Danger
onClicked: {
root.chatCommunitySectionModule.deleteCommunityMemberMessages(root.memberPubKey, "", "")
}
borderColor: "transparent"
}
StatusButton {
text: qsTr("Done")
onClicked: root.close()
}
}
}
}

View File

@ -26,3 +26,4 @@ TokenMasterActionPopup 1.0 TokenMasterActionPopup.qml
TokenPermissionsPopup 1.0 TokenPermissionsPopup.qml
TransferOwnershipPopup 1.0 TransferOwnershipPopup.qml
TransferOwnershipAlertPopup 1.0 TransferOwnershipAlertPopup.qml
CommunityMemberMessagesPopup 1.0 CommunityMemberMessagesPopup.qml

View File

@ -252,7 +252,7 @@ StatusSectionLayout {
mintPanel.openNewTokenForm(false/*Collectible owner token*/)
}
onShardIndexEdited: if (root.community.shardIndex != shardIndex) {
onShardIndexEdited: if (root.community.shardIndex !== shardIndex) {
root.chatCommunitySectionModule.setCommunityShard(shardIndex)
}
}
@ -277,6 +277,10 @@ StatusSectionLayout {
onUnbanUserClicked: root.rootStore.unbanUserFromCommunity(id)
onAcceptRequestToJoin: root.rootStore.acceptRequestToJoinCommunity(id, root.community.id)
onDeclineRequestToJoin: root.rootStore.declineRequestToJoinCommunity(id, root.community.id)
onViewMemberMessagesClicked: {
root.rootStore.loadCommunityMemberMessages(root.community.id, pubKey)
Global.openCommunityMemberMessagesPopupRequested(root.rootStore, root.chatCommunitySectionModule, pubKey, displayName)
}
}
PermissionsSettingsPanel {

View File

@ -1413,6 +1413,15 @@ Item {
}
}
Connections {
target: Global
function onSwitchToCommunityChannelsView(communityId: string) {
if (communityId !== model.id)
return
chatLayoutComponent.currentIndex = 0
}
}
sendModalPopup: sendModal
emojiPopup: statusEmojiPopup.item
stickersPopup: statusStickersPopupLoader.item

View File

@ -86,6 +86,7 @@ QtObject {
Global.openFirstTokenReceivedPopup.connect(openFirstTokenReceivedPopup)
Global.openConfirmHideAssetPopup.connect(openConfirmHideAssetPopup)
Global.openConfirmHideCollectiblePopup.connect(openConfirmHideCollectiblePopup)
Global.openCommunityMemberMessagesPopupRequested.connect(openCommunityMemberMessagesPopup)
}
property var currentPopup
@ -362,6 +363,15 @@ QtObject {
openPopup(confirmHideCollectiblePopup, { collectibleSymbol, collectibleName, collectibleImage, isCommunityToken })
}
function openCommunityMemberMessagesPopup(store, chatCommunitySectionModule, memberPubKey, displayName) {
openPopup(communityMemberMessagesPopup, {
store: store,
chatCommunitySectionModule: chatCommunitySectionModule,
memberPubKey: memberPubKey,
displayName: displayName
})
}
readonly property list<Component> _components: [
Component {
id: removeContactConfirmationDialog
@ -1135,6 +1145,12 @@ QtObject {
"")
}
}
},
Component {
id: communityMemberMessagesPopup
CommunityMemberMessagesPopup {
onClosed: destroy()
}
}
]
}

View File

@ -24,6 +24,7 @@ Loader {
property var usersStore
property var contactsStore
property var chatContentModule
property var chatCommunitySectionModule
property string channelEmoji
@ -96,6 +97,7 @@ Loader {
// External behavior changers
property bool isInPinnedPopup: false // The pinned popup limits the number of buttons shown
property bool isViewMemberMessagesePopup: false // The view member messages popup limits the number of buttons
property bool disableHover: false // Used to force the HoverHandler to be active (useful for messages in popups)
property bool placeholderMessage: false
@ -261,7 +263,10 @@ Loader {
property string activeMessage
readonly property bool isMessageActive: d.activeMessage === root.messageId
readonly property bool addReactionAllowed: !root.isInPinnedPopup && root.chatContentModule.chatDetails.canPostReactions
readonly property bool addReactionAllowed: !root.isInPinnedPopup &&
root.chatContentModule.chatDetails.canPostReactions &&
!root.isViewMemberMessagesePopup
function nextMessageHasHeader() {
if(!root.nextMessageAsJsonObj) {
@ -568,7 +573,7 @@ Loader {
Layout.bottomMargin: 16
messageTimestamp: root.messageTimestamp
previousMessageTimestamp: root.prevMessageIndex === -1 ? 0 : root.prevMessageTimestamp
visible: text !== "" && !root.isInPinnedPopup
visible: text !== "" && !root.isInPinnedPopup && !root.isViewMemberMessagesePopup
}
StatusMessage {
@ -910,7 +915,8 @@ Loader {
quickActions: [
Loader {
active: d.addReactionAllowed && delegate.hovered
active: d.addReactionAllowed && delegate.hovered && !root.isViewMemberMessagesePopup
visible: active
sourceComponent: StatusFlatRoundButton {
width: d.chatButtonSize
@ -925,7 +931,7 @@ Loader {
},
Loader {
active: !root.isInPinnedPopup && delegate.hovered && !delegate.hideQuickActions
&& root.rootStore.permissionsStore.viewAndPostCriteriaMet
&& !root.isViewMemberMessagesePopup && root.rootStore.permissionsStore.viewAndPostCriteriaMet
visible: active
sourceComponent: StatusFlatRoundButton {
objectName: "replyToMessageButton"
@ -941,7 +947,7 @@ Loader {
},
Loader {
active: !root.isInPinnedPopup && root.isText && !root.editModeOn && root.amISender && delegate.hovered && !delegate.hideQuickActions
&& root.rootStore.permissionsStore.viewAndPostCriteriaMet
&& !root.isViewMemberMessagesePopup && root.rootStore.permissionsStore.viewAndPostCriteriaMet
visible: active
sourceComponent: StatusFlatRoundButton {
objectName: "editMessageButton"
@ -969,6 +975,10 @@ Loader {
if (!root.rootStore.permissionsStore.viewAndPostCriteriaMet)
return false;
if (root.isViewMemberMessagesePopup) {
return false
}
const chatType = root.messageStore.chatType;
const pinMessageAllowedForMembers = root.messageStore.isPinMessageAllowedForMembers
@ -1007,7 +1017,7 @@ Loader {
}
},
Loader {
active: !root.editModeOn && delegate.hovered && !delegate.hideQuickActions
active: !root.editModeOn && delegate.hovered && !delegate.hideQuickActions && !root.isViewMemberMessagesePopup
visible: active
sourceComponent: StatusFlatRoundButton {
objectName: "markAsUnreadButton"
@ -1048,9 +1058,9 @@ Loader {
icon.name: "delete"
type: StatusFlatRoundButton.Type.Tertiary
tooltip.text: qsTr("Delete")
onClicked: {
messageStore.warnAndDeleteMessage(root.messageId)
}
onClicked: root.isViewMemberMessagesePopup
? root.chatCommunitySectionModule.deleteCommunityMemberMessages(root.senderId, root.messageId, root.chatId)
: messageStore.warnAndDeleteMessage(root.messageId)
}
}
]

View File

@ -81,6 +81,7 @@ QtObject {
signal openSendModal(string address)
signal switchToCommunity(string communityId)
signal switchToCommunitySettings(string communityId)
signal switchToCommunityChannelsView(string communityId)
signal createCommunityPopupRequested(bool isDiscordImport)
signal importCommunityPopupRequested()
signal communityIntroPopupRequested(string communityId, string name, string introMessage,
@ -101,6 +102,7 @@ QtObject {
signal openDeleteSavedAddressesPopup(var params)
signal openShowQRPopup(var params)
signal openSavedAddressActivityPopup(var params)
signal openCommunityMemberMessagesPopupRequested(var store, var chatCommunitySectionModule, var memberPubKey, var displayName)
function openProfilePopup(publicKey, parentPopup, cb) {
root.openProfilePopupRequested(publicKey, parentPopup, cb)

2
vendor/status-go vendored

@ -1 +1 @@
Subproject commit 78bf40994a482e5bf46c4c67ae4deb16065581f8
Subproject commit ad342c8887b0cc6fd91cac73919f45ac951efca9