diff --git a/src/app/global/feature_flags.nim b/src/app/global/feature_flags.nim
index 39543476f4..4f3a2f15f6 100644
--- a/src/app/global/feature_flags.nim
+++ b/src/app/global/feature_flags.nim
@@ -24,6 +24,7 @@ const DEFAULT_FLAG_LOCAL_BACKUP_ENABLED = true
const DEFAULT_FLAG_PRIVACY_MODE_FEATURE_ENABLED = true
const DEFAULT_FLAG_MESSAGE_LINK_SHARING_ENABLED = true
const DEFAULT_FLAG_STATUS_SUPPORT_BOT_ENABLED = true
+const DEFAULT_FLAG_THREADS_ENABLED = false
# Compile time feature flags
const DEFAULT_FLAG_DAPPS_ENABLED = true
@@ -44,6 +45,7 @@ featureFlag("LOCAL_BACKUP_ENABLED", DEFAULT_FLAG_LOCAL_BACKUP_ENABLED)
featureFlag("PRIVACY_MODE_FEATURE_ENABLED", DEFAULT_FLAG_PRIVACY_MODE_FEATURE_ENABLED)
featureFlag("MESSAGE_LINK_SHARING_ENABLED", DEFAULT_FLAG_MESSAGE_LINK_SHARING_ENABLED)
featureFlag("STATUS_SUPPORT_BOT_ENABLED", DEFAULT_FLAG_STATUS_SUPPORT_BOT_ENABLED)
+featureFlag("THREADS_ENABLED", DEFAULT_FLAG_THREADS_ENABLED)
featureFlag("DAPPS_ENABLED", DEFAULT_FLAG_DAPPS_ENABLED, true)
featureFlag("BROWSER_ENABLED", DEFAULT_FLAG_BROWSER_ENABLED, true)
@@ -100,6 +102,7 @@ QtObject:
messageLinkSharingEnabled: bool
statusSupportBotEnabled: bool
buyEnabled: bool
+ threadsEnabled: bool
proc setup(self: FeatureFlags) =
self.QObject.setup()
@@ -117,6 +120,7 @@ QtObject:
self.messageLinkSharingEnabled = MESSAGE_LINK_SHARING_ENABLED
self.statusSupportBotEnabled = STATUS_SUPPORT_BOT_ENABLED
self.buyEnabled = BUY_ENABLED
+ self.threadsEnabled = THREADS_ENABLED
proc newFeatureFlags*(): FeatureFlags =
new(result)
@@ -208,3 +212,9 @@ QtObject:
proc getBuyEnabled*(self: FeatureFlags): bool {.slot.} =
return self.buyEnabled
+
+ QtProperty[bool] threadsEnabled:
+ read = getThreadsEnabled
+
+ proc getThreadsEnabled*(self: FeatureFlags): bool {.slot.} =
+ return self.threadsEnabled
diff --git a/src/app/modules/main/chat_section/chat_content/controller.nim b/src/app/modules/main/chat_section/chat_content/controller.nim
index afdc51a687..8b0df5a64e 100644
--- a/src/app/modules/main/chat_section/chat_content/controller.nim
+++ b/src/app/modules/main/chat_section/chat_content/controller.nim
@@ -198,6 +198,12 @@ proc init*(self: Controller) =
return
self.delegate.onMessageEdited(args.message)
+ self.events.on(SIGNAL_CHAT_THREADS_LOADED) do(e: Args):
+ let args = ChatThreadsLoadedArgs(e)
+ if self.chatId != args.chatId:
+ return
+ self.delegate.onChatThreadsLoaded(args.threads)
+
proc getMyChatId*(self: Controller): string =
return self.chatId
diff --git a/src/app/modules/main/chat_section/chat_content/input_area/controller.nim b/src/app/modules/main/chat_section/chat_content/input_area/controller.nim
index 90ce2849fd..7d0a9115a0 100644
--- a/src/app/modules/main/chat_section/chat_content/input_area/controller.nim
+++ b/src/app/modules/main/chat_section/chat_content/input_area/controller.nim
@@ -131,7 +131,8 @@ proc sendImages*(self: Controller,
replyTo: string,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview],
- paymentRequests: seq[PaymentRequest]) =
+ paymentRequests: seq[PaymentRequest],
+ threadId: string = "") =
self.resetLinkPreviews()
self.chatService.asyncSendImages(
self.chatId,
@@ -140,7 +141,8 @@ proc sendImages*(self: Controller,
replyTo,
preferredUsername,
linkPreviews,
- paymentRequests
+ paymentRequests,
+ threadId
)
proc sendChatMessage*(self: Controller,
@@ -149,7 +151,8 @@ proc sendChatMessage*(self: Controller,
contentType: int,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview],
- paymentRequests: seq[PaymentRequest]) =
+ paymentRequests: seq[PaymentRequest],
+ threadId: string) =
self.resetLinkPreviews()
self.chatService.asyncSendChatMessage(self.chatId,
msg,
@@ -157,7 +160,8 @@ proc sendChatMessage*(self: Controller,
contentType,
preferredUsername,
linkPreviews,
- paymentRequests
+ paymentRequests,
+ threadId = threadId
)
proc getLinkPreviewEnabled*(self: Controller): bool =
diff --git a/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim b/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim
index 512afb4f9f..4b30b9c8f2 100644
--- a/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim
+++ b/src/app/modules/main/chat_section/chat_content/input_area/io_interface.nim
@@ -20,10 +20,15 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
-method sendChatMessage*(self: AccessInterface, msg: string, replyTo: string, contentType: int, linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) {.base.} =
+method sendChatMessage*(self: AccessInterface, msg: string, replyTo: string, contentType: int,
+ linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) {.base.} =
raise newException(ValueError, "No implementation available")
-method sendImages*(self: AccessInterface, imagePathsJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) {.base.} =
+method sendImages*(self: AccessInterface, imagePathsJson: string, msg: string, replyTo: string,
+ linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method getThreadId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
method searchGifs*(self: AccessInterface, query: string) {.base.} =
@@ -56,7 +61,7 @@ method searchGifsStarted*(self: AccessInterface) {.base.} =
method searchGifsError*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
-method serachGifsDone*(self: AccessInterface, gifs: seq[GifDto]) {.base.} =
+method searchGifsDone*(self: AccessInterface, gifs: seq[GifDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method getFavoritesGifs*(self: AccessInterface): seq[GifDto] {.base.} =
diff --git a/src/app/modules/main/chat_section/chat_content/input_area/module.nim b/src/app/modules/main/chat_section/chat_content/input_area/module.nim
index ebc280c946..f4a9cd247a 100644
--- a/src/app/modules/main/chat_section/chat_content/input_area/module.nim
+++ b/src/app/modules/main/chat_section/chat_content/input_area/module.nim
@@ -21,6 +21,7 @@ type
viewVariant: QVariant
controller: Controller
moduleLoaded: bool
+ threadId: string
proc newModule*(
delegate: delegate_interface.AccessInterface,
@@ -32,7 +33,8 @@ proc newModule*(
communityService: community_service.Service,
contactService: contact_service.Service,
messageService: message_service.Service,
- settingsService: settings_service.Service
+ settingsService: settings_service.Service,
+ threadId: string = ""
):
Module =
result = Module()
@@ -41,6 +43,7 @@ proc newModule*(
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, chatService, communityService, contactService, messageService, settingsService)
result.moduleLoaded = false
+ result.threadId = threadId
method delete*(self: Module) =
self.view.delete
@@ -66,8 +69,17 @@ method getModuleAsVariant*(self: Module): QVariant =
proc getChatId*(self: Module): string =
return self.controller.getChatId()
-method sendImages*(self: Module, imagePathsJson: string, msg: string, replyTo: string, linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) =
- self.controller.sendImages(imagePathsJson, msg, replyTo, singletonInstance.userProfile.getPreferredName(), linkPreviews, paymentRequests)
+method sendImages*(self: Module, imagePathsJson: string, msg: string, replyTo: string,
+ linkPreviews: seq[LinkPreview], paymentRequests: seq[PaymentRequest]) =
+ self.controller.sendImages(
+ imagePathsJson = imagePathsJson,
+ msg = msg,
+ replyTo = replyTo,
+ preferredUsername = singletonInstance.userProfile.getPreferredName(),
+ linkPreviews = linkPreviews,
+ paymentRequests = paymentRequests,
+ self.threadId,
+ )
method sendChatMessage*(
self: Module,
@@ -75,9 +87,20 @@ method sendChatMessage*(
replyTo: string,
contentType: int,
linkPreviews: seq[LinkPreview],
- paymentRequests: seq[PaymentRequest]) =
- self.controller.sendChatMessage(msg, replyTo, contentType,
- singletonInstance.userProfile.getPreferredName(), linkPreviews, paymentRequests)
+ paymentRequests: seq[PaymentRequest],
+ ) =
+ self.controller.sendChatMessage(
+ msg = msg,
+ replyTo = replyTo,
+ contentType = contentType,
+ preferredUsername = singletonInstance.userProfile.getPreferredName(),
+ linkPreviews,
+ paymentRequests,
+ self.threadId,
+ )
+
+method getThreadId*(self: Module): string =
+ return self.threadId
method setText*(self: Module, text: string, unfurlNewUrls: bool) =
self.controller.setText(text, unfurlNewUrls)
diff --git a/src/app/modules/main/chat_section/chat_content/input_area/view.nim b/src/app/modules/main/chat_section/chat_content/input_area/view.nim
index aaf226d481..22b6d02561 100644
--- a/src/app/modules/main/chat_section/chat_content/input_area/view.nim
+++ b/src/app/modules/main/chat_section/chat_content/input_area/view.nim
@@ -20,6 +20,7 @@ QtObject:
urlsModelVariant: QVariant
sendingInProgress: bool
askToEnableLinkPreview: bool
+ threadId: string
proc setSendingInProgress*(self: View, value: bool)
@@ -37,6 +38,7 @@ QtObject:
result.urlsModel = newUrlsModel()
result.urlsModelVariant = newQVariant(result.urlsModel)
result.askToEnableLinkPreview = false
+ result.threadId = ""
proc load*(self: View) =
self.delegate.viewDidLoad()
@@ -57,6 +59,12 @@ QtObject:
self.delegate.setText(msg, false)
self.delegate.sendImages(imagePathsJson, msg, replyTo, self.linkPreviewModel.getUnfuledLinkPreviews(), self.payment_request_model.getPaymentRequests())
+ proc getThreadId(self: View): string {.slot.} =
+ return self.delegate.getThreadId()
+
+ QtProperty[string] threadId:
+ read = getThreadId
+
proc getPreservedProperties(self: View): QVariant {.slot.} =
return self.preservedPropertiesVariant
diff --git a/src/app/modules/main/chat_section/chat_content/io_interface.nim b/src/app/modules/main/chat_section/chat_content/io_interface.nim
index 685233822d..0ccbfde9ee 100644
--- a/src/app/modules/main/chat_section/chat_content/io_interface.nim
+++ b/src/app/modules/main/chat_section/chat_content/io_interface.nim
@@ -5,6 +5,7 @@ import app_service/service/message/dto/pinned_message
import app_service/service/chat/dto/chat
import app_service/service/message/dto/message
import app_service/service/message/dto/reaction
+import app_service/service/message/dto/thread
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@@ -27,6 +28,12 @@ method getModuleAsVariant*(self: AccessInterface): QVariant {.base.} =
method onNotificationsUpdated*(self: AccessInterface, hasUnreadMessages: bool, notificationCount: int) {.base.} =
raise newException(ValueError, "No implementation available")
+method openThreadAsChat*(self: AccessInterface, threadId: string, threadName: string, parentMessageId: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onChatThreadsLoaded*(self: AccessInterface, threads: seq[ThreadDto]) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
method newPinnedMessagesLoaded*(self: AccessInterface, pinnedMessages: seq[PinnedMessageDto], reactions: seq[ReactionDto]) {.base.} =
raise newException(ValueError, "No implementation available")
diff --git a/src/app/modules/main/chat_section/chat_content/messages/controller.nim b/src/app/modules/main/chat_section/chat_content/messages/controller.nim
index fcf6d382d9..e3a621ea10 100644
--- a/src/app/modules/main/chat_section/chat_content/messages/controller.nim
+++ b/src/app/modules/main/chat_section/chat_content/messages/controller.nim
@@ -22,6 +22,7 @@ type
events: UniqueUUIDEventEmitter
sectionId: string
chatId: string
+ threadId: string
belongsToCommunity: bool
searchedMessageId: string
loadingMessagesPerPageFactor: int
@@ -35,12 +36,14 @@ type
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, contactService: contact_service.Service, communityService: community_service.Service,
chatService: chat_service.Service, messageService: message_service.Service,
- mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service): Controller =
+ mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service,
+ threadId: string = ""): Controller =
result = Controller()
result.delegate = delegate
result.events = initUniqueUUIDEventEmitter(events)
result.sectionId = sectionId
result.chatId = chatId
+ result.threadId = threadId
result.loadingMessagesPerPageFactor = 1
result.belongsToCommunity = belongsToCommunity
result.contactService = contactService
@@ -72,7 +75,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_SENDING_SUCCESS) do(e:Args):
let args = MessageSendingSuccess(e)
- if self.chatId != args.chat.id:
+ if self.chatId != args.chat.id or args.message.threadId != self.threadId:
return
self.delegate.onSendingMessageSuccess(args.message)
@@ -235,12 +238,52 @@ proc init*(self: Controller) =
let args = GetMessageResult(e)
self.delegate.onGetMessageById(args.requestId, args.messageId, args.message, args.error)
+ self.events.on(SIGNAL_THREAD_CREATED) do(e: Args):
+ let args = ThreadCreatedArgs(e)
+ if self.chatId != args.chatId:
+ return
+ self.delegate.onThreadCreated(args.parentMessageId, args.threads)
+
+ self.events.on(SIGNAL_THREAD_MESSAGES_LOADED) do(e: Args):
+ let args = ThreadMessagesLoadedArgs(e)
+ if self.chatId != args.chatId:
+ return
+ if self.threadId != args.threadId:
+ return
+ self.delegate.newMessagesLoaded(args.messages, @[])
+
+ self.events.on(SIGNAL_CHAT_THREADS_LOADED) do(e: Args):
+ let args = ChatThreadsLoadedArgs(e)
+ if self.chatId != args.chatId:
+ return
+ self.delegate.onChatThreadsLoaded(args.threads)
+
proc getMySectionId*(self: Controller): string =
return self.sectionId
proc getMyChatId*(self: Controller): string =
return self.chatId
+proc getMyThreadId*(self: Controller): string =
+ return self.threadId
+
+proc setThreadId*(self: Controller, threadId: string) =
+ self.threadId = threadId
+
+proc createThread*(self: Controller, parentMessageId: string) =
+ if parentMessageId.len == 0:
+ return
+ self.messageService.asyncCreateThread(self.chatId, parentMessageId)
+
+proc loadChatThreadsIfNeeded*(self: Controller) =
+ self.messageService.loadChatThreadsIfNeeded(self.chatId)
+
+proc hasThreadForParentMessage*(self: Controller, parentMessageId: string): bool =
+ return self.messageService.chatHasThreadForParentMessage(self.chatId, parentMessageId)
+
+proc closeThread*(self: Controller) =
+ self.threadId = ""
+
proc getChatDetails*(self: Controller): lent ChatDto =
return self.chatService.getChatById(self.chatId)
@@ -264,6 +307,9 @@ proc belongsToCommunity*(self: Controller): bool =
proc loadMoreMessages*(self: Controller): bool =
let limit = self.loadingMessagesPerPageFactor * MESSAGES_PER_PAGE
+ # TODO is it possible to just extend the existing messages API?
+ if self.threadId.len > 0:
+ return self.messageService.asyncLoadMoreMessagesForThread(self.chatId, self.threadId, limit)
return self.messageService.asyncLoadMoreMessagesForChat(self.chatId, limit)
proc addReaction*(self: Controller, messageId: string, emoji: string) =
diff --git a/src/app/modules/main/chat_section/chat_content/messages/io_interface.nim b/src/app/modules/main/chat_section/chat_content/messages/io_interface.nim
index f0a455614a..6338637eb8 100644
--- a/src/app/modules/main/chat_section/chat_content/messages/io_interface.nim
+++ b/src/app/modules/main/chat_section/chat_content/messages/io_interface.nim
@@ -1,8 +1,9 @@
import nimqml, uuids
-import ../../../../../../app_service/service/message/dto/[message, reaction, pinned_message]
-import ../../../../../../app_service/service/community/dto/community
-import ../../../../../../app_service/common/types
+import app_service/service/message/dto/[message, reaction, pinned_message]
+import app_service/service/message/dto/thread
+import app_service/service/community/dto/community
+import app_service/common/types
type
AccessInterface* {.pure inheritable.} = ref object of RootObj
@@ -105,6 +106,9 @@ method getSectionId*(self: AccessInterface): string {.base.} =
method getChatId*(self: AccessInterface): string {.base.} =
raise newException(ValueError, "No implementation available")
+method getThreadId*(self: AccessInterface): string {.base.} =
+ raise newException(ValueError, "No implementation available")
+
method getChatType*(self: AccessInterface): int {.base.} =
raise newException(ValueError, "No implementation available")
@@ -188,3 +192,18 @@ method onGetMessageById*(self: AccessInterface, requestId: UUID, messageId: stri
method forceLinkPreviewsLocalData*(self: AccessInterface, messageId: string) {.base.} =
raise newException(ValueError, "No implementation available")
+
+method createThread*(self: AccessInterface, parentMessageId: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method closeThread*(self: AccessInterface) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method setThreadId*(self: AccessInterface, threadId: string) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onThreadCreated*(self: AccessInterface, parentMessageId: string, threads: seq[ThreadDto]) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method onChatThreadsLoaded*(self: AccessInterface, threads: seq[ThreadDto]) {.base.} =
+ raise newException(ValueError, "No implementation available")
diff --git a/src/app/modules/main/chat_section/chat_content/messages/module.nim b/src/app/modules/main/chat_section/chat_content/messages/module.nim
index fe3ca7f4f8..551649b06b 100644
--- a/src/app/modules/main/chat_section/chat_content/messages/module.nim
+++ b/src/app/modules/main/chat_section/chat_content/messages/module.nim
@@ -11,6 +11,7 @@ import ../../../../../../app_service/service/contacts/service as contact_service
import ../../../../../../app_service/service/community/service as community_service
import ../../../../../../app_service/service/chat/service as chat_service
import ../../../../../../app_service/service/message/service as message_service
+import ../../../../../../app_service/service/message/dto/thread
import ../../../../../../app_service/service/mailservers/service as mailservers_service
import ../../../../../../app_service/service/shared_urls/service as shared_urls_service
import ../../../../../../app_service/service/contacts/dto/contact_details
@@ -47,14 +48,15 @@ type
proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
belongsToCommunity: bool, contactService: contact_service.Service, communityService: community_service.Service,
chatService: chat_service.Service, messageService: message_service.Service,
- mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service):
+ mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service,
+ threadId: string = ""):
Module =
result = Module()
result.delegate = delegate
result.view = view.newView(result)
result.viewVariant = newQVariant(result.view)
result.controller = controller.newController(result, events, sectionId, chatId, belongsToCommunity, contactService,
- communityService, chatService, messageService, mailserversService, sharedUrlsService)
+ communityService, chatService, messageService, mailserversService, sharedUrlsService, threadId = threadId)
result.moduleLoaded = false
result.initialMessagesLoaded = false
result.firstUnseenMessageState = (false, false, false)
@@ -82,8 +84,14 @@ method isLoaded*(self: Module): bool =
return self.moduleLoaded
method viewDidLoad*(self: Module) =
+ self.controller.loadChatThreadsIfNeeded()
+ self.view.setThreadId(self.controller.getMyThreadId())
+
+ if self.controller.getMyThreadId().len > 0:
+ discard self.controller.loadMoreMessages()
+
let chatDto = self.controller.getChatDetails()
- if chatDto.hasMoreMessagesToRequest():
+ if self.controller.getMyThreadId().len == 0 and chatDto.hasMoreMessagesToRequest():
self.view.model().insertItemBasedOnClock(self.createFetchMoreMessagesItem())
self.updateChatIdentifier()
@@ -159,6 +167,8 @@ proc createMessageItemsFromMessageDtos(self: Module, messages: seq[MessageDto],
transactionValue,
)
+ item.hasThread = self.controller.hasThreadForParentMessage(message.id)
+
self.updateLinkPreviewsContacts(item, requestFromMailserver = item.seen)
self.updateLinkPreviewsCommunities(item, requestFromMailserver = item.seen)
@@ -264,17 +274,22 @@ proc currentUserWalletContainsAddress(self: Module, address: string): bool =
return false
method reevaluateViewLoadingState*(self: Module) =
+ let inThreadMode = self.controller.getMyThreadId().len > 0
let loading = not self.initialMessagesLoaded or
- not self.firstUnseenMessageState.initialized or
- self.firstUnseenMessageState.fetching or
+ (not inThreadMode and (not self.firstUnseenMessageState.initialized or
+ self.firstUnseenMessageState.fetching)) or
self.view.getMessageSearchOngoing()
self.view.setLoading(loading)
method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: seq[ReactionDto]) =
- if messages.len > 0:
- var viewItems = self.createMessageItemsFromMessageDtos(messages, reactions)
+ var filtered = messages
+ if self.controller.getMyThreadId().len > 0:
+ filtered = messages.filterIt(it.threadId == self.controller.getMyThreadId())
- if self.controller.getChatDetails().hasMoreMessagesToRequest():
+ if filtered.len > 0:
+ var viewItems = self.createMessageItemsFromMessageDtos(filtered, reactions)
+
+ if self.controller.getMyThreadId().len == 0 and self.controller.getChatDetails().hasMoreMessagesToRequest():
viewItems.add(self.createFetchMoreMessagesItem())
viewItems.add(self.createChatIdentifierItem())
self.view.model().removeItem(FETCH_MORE_MESSAGES_MESSAGE_ID)
@@ -290,7 +305,14 @@ method newMessagesLoaded*(self: Module, messages: seq[MessageDto], reactions: se
self.reevaluateViewLoadingState()
method messagesAdded*(self: Module, messages: seq[MessageDto]) =
- let items = self.createMessageItemsFromMessageDtos(messages)
+ var filtered = messages
+ if self.controller.getMyThreadId().len > 0:
+ filtered = messages.filterIt(it.threadId == self.controller.getMyThreadId())
+
+ if filtered.len == 0:
+ return
+
+ let items = self.createMessageItemsFromMessageDtos(filtered)
self.view.model().insertItemsBasedOnClock(items)
self.checkIfMessageLoadedAndScroll()
@@ -396,6 +418,63 @@ method getSectionId*(self: Module): string =
method getChatId*(self: Module): string =
return self.controller.getMyChatId()
+method getThreadId*(self: Module): string =
+ return self.controller.getMyThreadId()
+
+method setThreadId*(self: Module, threadId: string) =
+ if self.controller.getMyThreadId() == threadId:
+ return
+
+ self.controller.setThreadId(threadId)
+ self.view.setThreadId(threadId)
+ self.initialMessagesLoaded = false
+ self.view.model().clear()
+
+ if threadId.len == 0 and self.controller.getChatDetails().hasMoreMessagesToRequest():
+ self.view.model().insertItemBasedOnClock(self.createFetchMoreMessagesItem())
+
+ self.updateChatIdentifier()
+ discard self.controller.loadMoreMessages()
+ self.reevaluateViewLoadingState()
+
+method createThread*(self: Module, parentMessageId: string) =
+ if self.controller.hasThreadForParentMessage(parentMessageId):
+ # TODO get thread name from service
+ # threadId == parentMessageId; open the existing thread as a sub-channel chat
+ # Note: threadName would ideally come from ThreadDto, but for existing threads
+ # the module should already be loaded, so threadName is not used
+ self.delegate.openThreadAsChat(parentMessageId, "", parentMessageId)
+ return
+
+ self.controller.createThread(parentMessageId)
+
+method closeThread*(self: Module) =
+ self.setThreadId("")
+
+method onThreadCreated*(self: Module, parentMessageId: string, threads: seq[ThreadDto]) =
+ if threads.len == 0:
+ return
+
+ var selectedThread = threads[0]
+ if parentMessageId.len > 0:
+ for thread in threads:
+ if thread.parentMessageId == parentMessageId:
+ selectedThread = thread
+ break
+
+ if selectedThread.parentMessageId.len > 0:
+ self.view.model().setHasThread(selectedThread.parentMessageId, true)
+
+ # Open the freshly created thread as a sub-channel chat
+ self.delegate.openThreadAsChat(selectedThread.threadId, selectedThread.name, selectedThread.parentMessageId)
+
+method onChatThreadsLoaded*(self: Module, threads: seq[ThreadDto]) =
+ for thread in threads:
+ if thread.parentMessageId.len > 0:
+ self.view.model().setHasThread(thread.parentMessageId, true)
+ # Add the thread to the chat list so it appears as a sub-chat
+ self.delegate.openThreadAsChat(thread.threadId, thread.name, thread.parentMessageId)
+
method getChatType*(self: Module): int =
let chatDto = self.controller.getChatDetails()
return chatDto.chatType.int
diff --git a/src/app/modules/main/chat_section/chat_content/messages/view.nim b/src/app/modules/main/chat_section/chat_content/messages/view.nim
index 6c7191ef1a..97199e1457 100644
--- a/src/app/modules/main/chat_section/chat_content/messages/view.nim
+++ b/src/app/modules/main/chat_section/chat_content/messages/view.nim
@@ -10,6 +10,7 @@ QtObject:
delegate: io_interface.AccessInterface
model: Model
modelVariant: QVariant
+ threadId: string
messageSearchOngoing: bool
amIChatAdmin: bool
isPinMessageAllowedForMembers: bool
@@ -26,6 +27,7 @@ QtObject:
result.delegate = delegate
result.model = newModel()
result.modelVariant = newQVariant(result.model)
+ result.threadId = ""
result.messageSearchOngoing = false
result.amIChatAdmin = false
result.isPinMessageAllowedForMembers = false
@@ -92,6 +94,32 @@ QtObject:
proc getChatId*(self: View): string {.slot.} =
return self.delegate.getChatId()
+ proc threadIdChanged*(self: View) {.signal.}
+ proc getThreadId*(self: View): string {.slot.} =
+ return self.threadId
+ proc setThreadId*(self: View, value: string) {.slot.} =
+ self.threadId = value
+ self.threadIdChanged()
+
+ QtProperty[string] threadId:
+ read = getThreadId
+ notify = threadIdChanged
+
+ proc createThread*(self: View, parentMessageId: string) {.slot.} =
+ self.delegate.createThread(parentMessageId)
+
+ # QML uses this to enter an existing thread; forward to the module so
+ # controller/view/model state stay in sync.
+ proc setThreadIdFromUI*(self: View, value: string) {.slot.} =
+ self.delegate.setThreadId(value)
+
+ proc closeThread*(self: View) {.slot.} =
+ self.delegate.closeThread()
+
+ proc threadCreated*(self: View, threadId: string, parentMessageId: string) {.signal.}
+ proc emitThreadCreatedSignal*(self: View, threadId: string, parentMessageId: string) =
+ self.threadCreated(threadId, parentMessageId)
+
proc getNumberOfPinnedMessages*(self: View): int {.slot.} =
return self.delegate.getNumberOfPinnedMessages()
diff --git a/src/app/modules/main/chat_section/chat_content/module.nim b/src/app/modules/main/chat_section/chat_content/module.nim
index e93ef74dd0..4cd61ba976 100644
--- a/src/app/modules/main/chat_section/chat_content/module.nim
+++ b/src/app/modules/main/chat_section/chat_content/module.nim
@@ -14,15 +14,15 @@ import input_area/module as input_area_module
import messages/module as messages_module
import users/module as users_module
-import ../../../../../app_service/service/settings/service as settings_service
-import ../../../../../app_service/service/node_configuration/service as node_configuration_service
-import ../../../../../app_service/service/contacts/service as contact_service
-import ../../../../../app_service/service/chat/service as chat_service
-import ../../../../../app_service/service/community/service as community_service
-import ../../../../../app_service/service/message/service as message_service
-import ../../../../../app_service/service/mailservers/service as mailservers_service
-import ../../../../../app_service/service/shared_urls/service as shared_urls_service
-import ../../../../../app_service/common/types
+import app_service/service/settings/service as settings_service
+import app_service/service/node_configuration/service as node_configuration_service
+import app_service/service/contacts/service as contact_service
+import app_service/service/chat/service as chat_service
+import app_service/service/community/service as community_service
+import app_service/service/message/service as message_service
+import app_service/service/mailservers/service as mailservers_service
+import app_service/service/shared_urls/service as shared_urls_service
+import app_service/common/types
export io_interface
@@ -45,7 +45,8 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
nodeConfigurationService: node_configuration_service.Service, contactService: contact_service.Service,
chatService: chat_service.Service, communityService: community_service.Service,
messageService: message_service.Service,
- mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service):
+ mailserversService: mailservers_service.Service, sharedUrlsService: shared_urls_service.Service,
+ threadId: string = ""):
Module =
result = Module()
result.delegate = delegate
@@ -57,9 +58,9 @@ proc newModule*(delegate: delegate_interface.AccessInterface, events: EventEmitt
result.moduleLoaded = false
result.inputAreaModule = input_area_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
- chatService, communityService, contactService, messageService, settingsService)
+ chatService, communityService, contactService, messageService, settingsService, threadId = threadId)
result.messagesModule = messages_module.newModule(result, events, sectionId, chatId, belongsToCommunity,
- contactService, communityService, chatService, messageService, mailserversService, sharedUrlsService)
+ contactService, communityService, chatService, messageService, mailserversService, sharedUrlsService, threadId = threadId)
result.usersModule = users_module.newModule(events, sectionId, chatId, belongsToCommunity,
isUsersListAvailable, contactService, chat_service, communityService, messageService)
@@ -258,6 +259,15 @@ method onMessageEdited*(self: Module, message: MessageDto) =
method getMyChatId*(self: Module): string =
self.controller.getMyChatId()
+method openThreadAsChat*(self: Module, threadId: string, threadName: string, parentMessageId: string) =
+ self.delegate.openThreadAsChat(self.controller.getMyChatId(), threadId, threadName, parentMessageId, setActive = true)
+
+method onChatThreadsLoaded*(self: Module, threads: seq[ThreadDto]) =
+ for thread in threads:
+ if thread.parentMessageId.len > 0:
+ # Add the thread to the chat list so it appears as a sub-chat
+ self.delegate.openThreadAsChat(self.controller.getMyChatId(), thread.threadId, thread.name, thread.parentMessageId, setActive = false)
+
method muteChat*(self: Module, interval: int) =
self.controller.muteChat(interval)
diff --git a/src/app/modules/main/chat_section/controller.nim b/src/app/modules/main/chat_section/controller.nim
index 54f2b4bcab..e2ea4abd83 100644
--- a/src/app/modules/main/chat_section/controller.nim
+++ b/src/app/modules/main/chat_section/controller.nim
@@ -169,7 +169,7 @@ proc init*(self: Controller) =
self.contactService, self.chatService, self.communityService, self.messageService,
self.mailserversService, self.sharedUrlsService, setChatAsActive = true)
- if (self.isCommunitySection):
+ if self.isCommunitySection:
self.events.on(SIGNAL_COMMUNITY_CHANNEL_CREATED) do(e:Args):
let args = CommunityChatArgs(e)
let belongsToCommunity = args.chat.communityId.len > 0
@@ -488,7 +488,8 @@ proc setActiveItem*(self: Controller, itemId: string) =
self.delegate.activeItemSet(self.activeItemId)
if self.activeItemId != "":
- self.messageService.asyncLoadInitialMessagesForChat(self.activeItemId)
+ if not self.delegate.isChatThread(self.activeItemId):
+ self.messageService.asyncLoadInitialMessagesForChat(self.activeItemId)
proc removeCommunityChat*(self: Controller, itemId: string) =
self.communityService.deleteCommunityChat(self.getMySectionId(), itemId)
diff --git a/src/app/modules/main/chat_section/io_interface.nim b/src/app/modules/main/chat_section/io_interface.nim
index 4438bf0687..5771e8da81 100644
--- a/src/app/modules/main/chat_section/io_interface.nim
+++ b/src/app/modules/main/chat_section/io_interface.nim
@@ -171,6 +171,12 @@ method viewDidLoad*(self: AccessInterface) {.base.} =
method setActiveItem*(self: AccessInterface, itemId: string) {.base.} =
raise newException(ValueError, "No implementation available")
+method openThreadAsChat*(self: AccessInterface, parentChatId: string, threadId: string, threadName: string, parentMessageId: string, setActive: bool = false) {.base.} =
+ raise newException(ValueError, "No implementation available")
+
+method isChatThread*(self: AccessInterface, chatId: string): bool {.base.} =
+ raise newException(ValueError, "No implementation available")
+
method getChatContentModule*(self: AccessInterface, chatId: string): QVariant {.base.} =
raise newException(ValueError, "No implementation available")
diff --git a/src/app/modules/main/chat_section/item.nim b/src/app/modules/main/chat_section/item.nim
index be4d5fdacd..629e185cd5 100644
--- a/src/app/modules/main/chat_section/item.nim
+++ b/src/app/modules/main/chat_section/item.nim
@@ -42,6 +42,7 @@ type
missingEncryptionKey: bool
permissionsCheckOngoing: bool
hidden: bool # cached: row hidden by its collapsed category (see recomputeHidden)
+ isThread: bool
# Row is hidden by its collapsed category. Active, unmuted-unread and
# notification-carrying chats stay visible so the list can surface them.
@@ -88,6 +89,7 @@ proc initChatItem*(
hideIfPermissionsNotMet: bool = false,
missingEncryptionKey: bool = false,
permissionsCheckOngoing: bool = false,
+ isThread: bool = false,
): ChatItem =
result = ChatItem()
result.id = id
@@ -125,6 +127,7 @@ proc initChatItem*(
result.missingEncryptionKey = missingEncryptionKey
result.permissionsCheckOngoing = permissionsCheckOngoing
result.recomputeHidden()
+ result.isThread = isThread
proc `$`*(self: ChatItem): string =
result = fmt"""chat_section/ChatItem(
@@ -410,3 +413,6 @@ proc permissionsCheckOngoing*(self: ChatItem): bool =
proc `permissionsCheckOngoing=`*(self: var ChatItem, value: bool) =
self.permissionsCheckOngoing = value
+
+proc isThread*(self: ChatItem): bool =
+ self.isThread
diff --git a/src/app/modules/main/chat_section/model.nim b/src/app/modules/main/chat_section/model.nim
index 8e341ecc15..e5d4454cc1 100644
--- a/src/app/modules/main/chat_section/model.nim
+++ b/src/app/modules/main/chat_section/model.nim
@@ -263,6 +263,7 @@ QtObject:
if categoryIdx == -1:
return
indexToInsertTo = categoryIdx + item.position + 1
+
if indexToInsertTo < 0:
indexToInsertTo = 0
elif indexToInsertTo >= self.items.len + 1:
@@ -274,6 +275,20 @@ QtObject:
self.countChanged()
+ proc appendItemAfterParent*(self: Model, item: ChatItem, parentIndex: int) =
+ if parentIndex < 0 or parentIndex >= self.items.len:
+ return
+
+ let parentModelIndex = newQModelIndex()
+ defer: parentModelIndex.delete
+
+ let indexToInsertTo = parentIndex + 1
+ self.beginInsertRows(parentModelIndex, indexToInsertTo, indexToInsertTo)
+ self.items.insert(item, indexToInsertTo)
+ self.endInsertRows()
+
+ self.countChanged()
+
proc changeCategoryOpened*(self: Model, categoryId: string, opened: bool) {.slot.} =
for ind in 0 ..< self.items.len:
if self.items[ind].categoryId == categoryId:
diff --git a/src/app/modules/main/chat_section/module.nim b/src/app/modules/main/chat_section/module.nim
index 2329e68751..98ce0ebaf0 100644
--- a/src/app/modules/main/chat_section/module.nim
+++ b/src/app/modules/main/chat_section/module.nim
@@ -1,4 +1,4 @@
-import nimqml, tables, chronicles, json, sequtils, std/strformat, sugar, marshal
+import nimqml, tables, chronicles, json, sequtils, std/strformat, sugar, marshal, std/sets
from seaqt/qtimer import QTimer, create, setSingleShot, onTimeout, start, stop, isActive
import io_interface
@@ -56,6 +56,17 @@ type
# defers the first-activation model build off the tap handler (seaqt
# QTimer, auto-destroyed via =destroy)
initialBuildTimer: QTimer
+ # services retained so thread sub-channels can be created on demand
+ events: EventEmitter
+ settingsService: settings_service.Service
+ nodeConfigurationService: node_configuration_service.Service
+ contactService: contact_service.Service
+ chatService: chat_service.Service
+ communityService: community_service.Service
+ messageService: message_service.Service
+ mailserversService: mailservers_service.Service
+ sharedUrlsService: shared_urls_service.Service
+ threadChatIds: HashSet[string]
# Forward declaration
proc buildChatSectionUI(
@@ -124,6 +135,17 @@ proc newModule*(
result.moduleLoaded = false
result.chatsLoaded = false
+ result.events = events
+ result.settingsService = settingsService
+ result.nodeConfigurationService = nodeConfigurationService
+ result.contactService = contactService
+ result.chatService = chatService
+ result.communityService = communityService
+ result.messageService = messageService
+ result.mailserversService = mailserversService
+ result.sharedUrlsService = sharedUrlsService
+ result.threadChatIds = initHashSet[string]()
+
result.chatContentModules = initOrderedTable[string, chat_content_module.AccessInterface]()
if isCommunity:
result.membersListModule = users_module.newModule(events, sectionId, chatId = "", isCommunity,
@@ -447,6 +469,71 @@ method chatContentDidLoad*(self: Module) =
method setActiveItem*(self: Module, itemId: string) =
self.controller.setActiveItem(itemId)
+method isChatThread*(self: Module, chatId: string): bool =
+ return self.threadChatIds.contains(chatId)
+
+method openThreadAsChat*(self: Module, parentChatId: string, threadId: string, threadName: string, parentMessageId: string, setActive: bool = false) =
+ if threadId.len == 0:
+ return
+
+ # If the thread sub-channel already exists, just activate it.
+ if self.chatContentModules.contains(threadId) and setActive:
+ self.setActiveItem(threadId)
+ return
+
+ let parentItem = self.view.chatsModel().getItemById(parentChatId)
+ if parentItem.isNil:
+ error "openThreadAsChat: unknown parent chat", parentChatId, methodName="openThreadAsChat"
+ return
+
+ let parentIndex = self.view.chatsModel().getItemIdxById(parentChatId)
+ if parentIndex == -1:
+ error "openThreadAsChat: unknown parent chat index", parentChatId, methodName="openThreadAsChat"
+ return
+
+ let belongsToCommunity = self.controller.isCommunity()
+ let isUsersListAvailable = parentItem.`type` != ChatType.OneToOne.int
+
+ # The thread content module is keyed in the chat list by the threadId, but it
+ # loads/sends messages against the parent chat id together with the threadId.
+ self.chatContentModules[threadId] = chat_content_module.newModule(
+ self, self.events, self.controller.getMySectionId(), parentChatId,
+ belongsToCommunity, isUsersListAvailable, self.settingsService, self.nodeConfigurationService,
+ self.contactService, self.chatService, self.communityService, self.messageService,
+ self.mailserversService, self.sharedUrlsService, threadId = threadId)
+
+ self.threadChatIds.incl(threadId)
+
+ let threadItem = chat_item.initChatItem(
+ id = threadId,
+ name = "🧵 " & threadName,
+ usesDefaultName = false,
+ icon = parentItem.icon,
+ color = parentItem.color,
+ emoji = parentItem.emoji,
+ description = "",
+ `type` = parentItem.`type`,
+ parentItem.memberRole,
+ lastMessageTimestamp = 0,
+ lastMessageText = "",
+ hasUnreadMessages = false,
+ notificationsCount = 0,
+ muted = false,
+ blocked = false,
+ active = false,
+ position = parentItem.position,
+ categoryId = parentItem.categoryId,
+ categoryPosition = parentItem.categoryPosition,
+ canPost = parentItem.canPost,
+ canView = parentItem.canView,
+ canPostReactions = parentItem.canPostReactions,
+ isThread = true,
+ )
+
+ self.view.chatsModel().appendItemAfterParent(threadItem, parentIndex)
+ if setActive:
+ self.setActiveItem(threadId)
+
proc updateActiveChatMembership*(self: Module) =
let activeChatId = self.controller.getActiveChatId()
let chat = self.controller.getChatDetails(activeChatId)
diff --git a/src/app/modules/main/gifs/controller.nim b/src/app/modules/main/gifs/controller.nim
index 5dcc2e8799..93b3ce686f 100644
--- a/src/app/modules/main/gifs/controller.nim
+++ b/src/app/modules/main/gifs/controller.nim
@@ -48,7 +48,7 @@ proc init*(self: Controller) =
self.events.on(SIGNAL_SEARCH_GIFS_DONE) do(e:Args):
let args = GifsArgs(e)
- self.delegate.serachGifsDone(args.gifs)
+ self.delegate.searchGifsDone(args.gifs)
self.events.on(SIGNAL_SEARCH_GIFS_ERROR) do(e:Args):
self.delegate.searchGifsError()
diff --git a/src/app/modules/main/gifs/io_interface.nim b/src/app/modules/main/gifs/io_interface.nim
index 903e43c57c..aaffc19040 100644
--- a/src/app/modules/main/gifs/io_interface.nim
+++ b/src/app/modules/main/gifs/io_interface.nim
@@ -45,7 +45,7 @@ method searchGifsStarted*(self: AccessInterface) {.base.} =
method searchGifsError*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
-method serachGifsDone*(self: AccessInterface, gifs: seq[GifDto]) {.base.} =
+method searchGifsDone*(self: AccessInterface, gifs: seq[GifDto]) {.base.} =
raise newException(ValueError, "No implementation available")
method getFavoritesGifs*(self: AccessInterface): seq[GifDto] {.base.} =
diff --git a/src/app/modules/main/gifs/module.nim b/src/app/modules/main/gifs/module.nim
index 008c4aa0fc..4fd3852284 100644
--- a/src/app/modules/main/gifs/module.nim
+++ b/src/app/modules/main/gifs/module.nim
@@ -84,7 +84,7 @@ method searchGifsError*(self: Module) =
# Just setting loading to false works because the UI shows an error when there are no gifs
self.view.setGifLoading(false)
-method serachGifsDone*(self: Module, gifs: seq[GifDto]) =
+method searchGifsDone*(self: Module, gifs: seq[GifDto]) =
self.view.setGifLoading(false)
self.view.updateGifColumns(gifs)
diff --git a/src/app/modules/shared_models/message_item.nim b/src/app/modules/shared_models/message_item.nim
index 194303537a..2af5b89286 100644
--- a/src/app/modules/shared_models/message_item.nim
+++ b/src/app/modules/shared_models/message_item.nim
@@ -76,6 +76,7 @@ type
albumImagesCount: int
bridgeName: string
paymentRequestModel: payment_request_model.Model
+ hasThread: bool
proc initMessageItem*(
id,
@@ -132,6 +133,7 @@ proc initMessageItem*(
bridgeMessage: BridgeMessage,
quotedBridgeMessage: BridgeMessage,
paymentRequests: seq[PaymentRequest],
+ hasThread: bool = false,
): Item =
result = Item()
result.id = id
@@ -193,6 +195,7 @@ proc initMessageItem*(
result.albumMessageIds = albumMessageIds
result.albumImagesCount = albumImagesCount
result.paymentRequestModel = newPaymentRequestModel(paymentRequests)
+ result.hasThread = hasThread
if quotedMessageContentType == ContentType.DiscordMessage:
result.quotedMessageAuthorDisplayName = quotedMessageDiscordMessage.author.name
@@ -596,9 +599,16 @@ proc toJsonNode*(self: Item): JsonNode =
"albumMessageImages": self.albumMessageImages,
"albumMessageIds": self.albumMessageIds,
"albumImagesCount": self.albumImagesCount,
- "bridgeName": self.bridgeName
+ "bridgeName": self.bridgeName,
+ "hasThread": self.hasThread
}
+proc hasThread*(self: Item): bool {.inline.} =
+ self.hasThread
+
+proc `hasThread=`*(self: Item, value: bool) {.inline.} =
+ self.hasThread = value
+
proc editMode*(self: Item): bool {.inline.} =
self.editMode
diff --git a/src/app/modules/shared_models/message_model.nim b/src/app/modules/shared_models/message_model.nim
index f57791f7bd..ea84d0a5a0 100644
--- a/src/app/modules/shared_models/message_model.nim
+++ b/src/app/modules/shared_models/message_model.nim
@@ -75,6 +75,7 @@ type
BridgeName
PaymentRequestModel
CompressedKey
+ HasThread
QtObject:
type
@@ -185,6 +186,7 @@ QtObject:
ModelRole.BridgeName.int: "bridgeName",
ModelRole.PaymentRequestModel.int: "paymentRequestModel",
ModelRole.CompressedKey.int: "compressedKey",
+ ModelRole.HasThread.int: "hasThread",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@@ -356,6 +358,8 @@ QtObject:
result = newQVariant(item.paymentRequestModel)
of ModelRole.CompressedKey:
result = newQVariant(item.compressedKey)
+ of ModelRole.HasThread:
+ result = newQVariant(item.hasThread)
proc updateAdjacentMessageRolesAtIndex(self: Model, row: int) =
if row < 0 or row >= self.items.len:
@@ -628,6 +632,20 @@ QtObject:
updateRole(pinned)
updateRoleWithValue(pinnedBy, targetPinnedBy)
+ proc setHasThread*(self: Model, messageId: string, hasThread: bool) =
+ let ind = self.findIndexForMessageId(messageId)
+ if ind == -1:
+ return
+
+ if self.items[ind].hasThread == hasThread:
+ return
+
+ self.items[ind].hasThread = hasThread
+
+ let index = self.createIndex(ind, 0, nil)
+ defer: index.delete
+ self.dataChanged(index, index, @[ModelRole.HasThread.int])
+
proc getMessageByIdAsJson*(self: Model, messageId: string): JsonNode =
for it in self.items:
if(it.id == messageId):
@@ -1002,4 +1020,5 @@ QtObject:
message.bridgeMessage,
message.quotedMessage.bridgeMessage,
message.paymentRequests,
+ hasThread = false,
)
diff --git a/src/app_service/service/chat/async_tasks.nim b/src/app_service/service/chat/async_tasks.nim
index 99c0f8f18f..b8a316e54a 100644
--- a/src/app_service/service/chat/async_tasks.nim
+++ b/src/app_service/service/chat/async_tasks.nim
@@ -68,6 +68,7 @@ type
chatId: string
processedMsg: string
replyTo: string
+ threadId: string
contentType: int
preferredUsername: string
communityId: string
@@ -84,6 +85,7 @@ const asyncSendMessageTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
arg.processedMsg,
arg.replyTo,
arg.contentType,
+ arg.threadId,
arg.preferredUsername,
arg.standardLinkPreviews,
arg.statusLinkPreviews,
@@ -107,6 +109,7 @@ type
imagePathsJson: string
msg: string
replyTo: string
+ threadId: string
preferredUsername: string
standardLinkPreviews: JsonNode
statusLinkPreviews: JsonNode
@@ -138,6 +141,7 @@ const asyncSendImagesTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} =
arg.msg,
arg.replyTo,
arg.preferredUsername,
+ arg.threadId,
arg.standardLinkPreviews,
arg.statusLinkPreviews,
arg.paymentRequests
diff --git a/src/app_service/service/chat/service.nim b/src/app_service/service/chat/service.nim
index 940f300dad..e8ec198b32 100644
--- a/src/app_service/service/chat/service.nim
+++ b/src/app_service/service/chat/service.nim
@@ -425,7 +425,8 @@ QtObject:
replyTo: string,
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[],
- paymentRequests: seq[PaymentRequest] = @[]) =
+ paymentRequests: seq[PaymentRequest] = @[],
+ threadId: string = "") =
try:
let (standardLinkPreviews, statusLinkPreviews) = extractLinkPreviewsLists(linkPreviews)
@@ -437,6 +438,7 @@ QtObject:
imagePathsJson: imagePathsJson,
msg: msg,
replyTo: replyTo,
+ threadId: threadId,
preferredUsername: preferredUsername,
standardLinkPreviews: %standardLinkPreviews,
statusLinkPreviews: %statusLinkPreviews,
@@ -474,7 +476,8 @@ QtObject:
preferredUsername: string = "",
linkPreviews: seq[LinkPreview] = @[],
paymentRequests: seq[PaymentRequest] = @[],
- communityId: string = "") =
+ communityId: string = "",
+ threadId: string = "") =
try:
let allKnownContacts = self.contactService.getContactsByGroup(ContactsGroup.AllKnownContacts)
let processedMsg = message_common.replaceMentionsWithPubKeys(allKnownContacts, msg)
@@ -488,6 +491,7 @@ QtObject:
chatId: chatId,
processedMsg: processedMsg,
replyTo: replyTo,
+ threadId: threadId,
contentType: contentType,
preferredUsername: preferredUsername,
communityId: communityId, # Only send a community ID for the community invites
diff --git a/src/app_service/service/message/async_tasks.nim b/src/app_service/service/message/async_tasks.nim
index d997207e75..8e22b4917b 100644
--- a/src/app_service/service/message/async_tasks.nim
+++ b/src/app_service/service/message/async_tasks.nim
@@ -23,20 +23,29 @@ proc getCountAndCountWithMentionsFromResponse(chatId: string, seenAndUnseenMessa
type
AsyncFetchChatMessagesTaskArg = ref object of QObjectTaskArg
chatId: string
+ threadId: string
msgCursor: string
limit: int
+ AsyncFetchChatThreadsTaskArg = ref object of QObjectTaskArg
+ chatId: string
+
+ AsyncCreateThreadTaskArg = ref object of QObjectTaskArg
+ chatId: string
+ parentMessageId: string
+
proc asyncFetchChatMessagesTask(argEncoded: string) {.gcsafe, nimcall.} =
let arg = decode[AsyncFetchChatMessagesTaskArg](argEncoded)
try:
var responseJson = %*{
- "chatId": arg.chatId
+ "chatId": arg.chatId,
+ "threadId": arg.threadId,
}
# handle messages
var messagesArr: JsonNode
var messagesCursor: JsonNode
- let msgsResponse = status_go.fetchMessages(arg.chatId, arg.msgCursor, arg.limit)
+ let msgsResponse = status_go.fetchMessages(arg.chatId, arg.threadId, arg.msgCursor, arg.limit)
if not msgsResponse.error.isNil:
raise newException(CatchableError, msgsResponse.error.message)
@@ -46,20 +55,70 @@ proc asyncFetchChatMessagesTask(argEncoded: string) {.gcsafe, nimcall.} =
responseJson["messages"] = messagesArr
responseJson["messagesCursor"] = messagesCursor
- # handle reactions
- var reactionsArr: JsonNode
- let rResponse = status_go.fetchReactions(arg.chatId, arg.msgCursor, arg.limit)
- if not rResponse.error.isNil:
- raise newException(CatchableError, rResponse.error.message)
-
- reactionsArr = rResponse.result
- responseJson["reactions"] = reactionsArr
+ # handle reactions (only for base chats, not threads)
+ if arg.threadId == "":
+ var reactionsArr: JsonNode
+ let rResponse = status_go.fetchReactions(arg.chatId, arg.msgCursor, arg.limit)
+ if not rResponse.error.isNil:
+ raise newException(CatchableError, rResponse.error.message)
+ responseJson["reactions"] = rResponse.result
arg.finish(responseJson)
except Exception as e:
arg.finish(%* {
"chatId": arg.chatId,
+ "threadId": arg.threadId,
+ "error": e.msg,
+ })
+
+proc asyncFetchChatThreadsTask(argEncoded: string) {.gcsafe, nimcall.} =
+ let arg = decode[AsyncFetchChatThreadsTaskArg](argEncoded)
+ try:
+ let response = status_go_chat.fetchChatThreads(arg.chatId)
+ if not response.error.isNil:
+ raise newException(CatchableError, response.error.message)
+
+ var responseJson = %*{
+ "chatId": arg.chatId,
+ "threads": %*[],
+ "error": "",
+ }
+
+ var threadsArr: JsonNode
+ if response.result.getProp("threads", threadsArr):
+ responseJson["threads"] = threadsArr
+
+ arg.finish(responseJson)
+ except Exception as e:
+ arg.finish(%* {
+ "chatId": arg.chatId,
+ "error": e.msg,
+ })
+
+proc asyncCreateThreadTask(argEncoded: string) {.gcsafe, nimcall.} =
+ let arg = decode[AsyncCreateThreadTaskArg](argEncoded)
+ try:
+ let response = status_go_chat.createThread(arg.chatId, arg.parentMessageId)
+ if not response.error.isNil:
+ raise newException(CatchableError, response.error.message)
+
+ var responseJson = %*{
+ "chatId": arg.chatId,
+ "parentMessageId": arg.parentMessageId,
+ "threads": %*[],
+ "error": "",
+ }
+
+ var threadsArr: JsonNode
+ if response.result.getProp("threads", threadsArr):
+ responseJson["threads"] = threadsArr
+
+ arg.finish(responseJson)
+ except Exception as e:
+ arg.finish(%* {
+ "chatId": arg.chatId,
+ "parentMessageId": arg.parentMessageId,
"error": e.msg,
})
diff --git a/src/app_service/service/message/dto/message.nim b/src/app_service/service/message/dto/message.nim
index f5dab88481..3c0e63208d 100644
--- a/src/app_service/service/message/dto/message.nim
+++ b/src/app_service/service/message/dto/message.nim
@@ -111,6 +111,7 @@ type MessageDto* = object
text*: string
chatId*: string
localChatId*: string
+ threadId*: string
clock*: int64
replace*: string
responseTo*: string
@@ -259,6 +260,7 @@ proc toMessageDto*(jsonObj: JsonNode): MessageDto =
discard jsonObj.getProp("text", result.text)
discard jsonObj.getProp("chatId", result.chatId)
discard jsonObj.getProp("localChatId", result.localChatId)
+ discard jsonObj.getProp("threadId", result.threadId)
discard jsonObj.getProp("clock", result.clock)
discard jsonObj.getProp("replace", result.replace)
discard jsonObj.getProp("responseTo", result.responseTo)
diff --git a/src/app_service/service/message/dto/thread.nim b/src/app_service/service/message/dto/thread.nim
new file mode 100644
index 0000000000..5dbeb3a4d9
--- /dev/null
+++ b/src/app_service/service/message/dto/thread.nim
@@ -0,0 +1,16 @@
+import json
+
+include ../../../common/json_utils
+
+type ThreadDto* = object
+ threadId*: string
+ chatId*: string
+ parentMessageId*: string
+ name*: string
+
+proc toThreadDto*(jsonObj: JsonNode): ThreadDto =
+ result = ThreadDto()
+ discard jsonObj.getProp("threadId", result.threadId)
+ discard jsonObj.getProp("chatId", result.chatId)
+ discard jsonObj.getProp("parentMessageId", result.parentMessageId)
+ discard jsonObj.getProp("name", result.name)
diff --git a/src/app_service/service/message/service.nim b/src/app_service/service/message/service.nim
index d7f843e496..fd47cbf740 100644
--- a/src/app_service/service/message/service.nim
+++ b/src/app_service/service/message/service.nim
@@ -1,4 +1,4 @@
-import nimqml, tables, json, regex, sequtils, std/strformat, strutils, chronicles, times, oids, uuids
+import nimqml, tables, json, regex, sequtils, std/strformat, strutils, chronicles, times, oids, uuids, sets
import ../../common/utils as common_utils
import ../../../app/core/tasks/[qt, threadpool]
@@ -15,6 +15,7 @@ import ../wallet_account/service as wallet_account_service
import ./dto/message as message_dto
import ./dto/pinned_message as pinned_msg_dto
import ./dto/reaction as reaction_dto
+import ./dto/thread as thread_dto
import ../chat/dto/chat as chat_dto
import ./dto/pinned_message_update as pinned_msg_update_dto
import ./dto/removed_message as removed_msg_dto
@@ -32,6 +33,7 @@ import web3/conversions
export message_dto
export pinned_msg_dto
export reaction_dto
+export thread_dto
logScope:
topics = "messages-service"
@@ -41,6 +43,12 @@ const MESSAGES_PER_PAGE_MAX* = 40
# Signals which may be emitted by this service:
const SIGNAL_MESSAGES_LOADED* = "messagesLoaded"
+const SIGNAL_CHAT_THREADS_LOADED* = "chatThreadsLoaded"
+const SIGNAL_CHAT_THREADS_LOADING_FAILED* = "chatThreadsLoadingFailed"
+const SIGNAL_THREAD_MESSAGES_LOADED* = "threadMessagesLoaded"
+const SIGNAL_THREAD_MESSAGES_LOADING_FAILED* = "threadMessagesLoadingFailed"
+const SIGNAL_THREAD_CREATED* = "threadCreated"
+const SIGNAL_THREAD_CREATION_FAILED* = "threadCreationFailed"
const SIGNAL_PINNED_MESSAGES_LOADED* = "pinnedMessagesLoaded"
const SIGNAL_REACTIONS_FOR_MESSAGE_LOADED* = "signalReactionsForMessageLoaded"
const SIGNAL_FIRST_UNSEEN_MESSAGE_LOADED* = "firstUnseenMessageLoaded"
@@ -83,6 +91,20 @@ type
messages*: seq[MessageDto]
reactions*: seq[ReactionDto]
+ ChatThreadsLoadedArgs* = ref object of Args
+ chatId*: string
+ threads*: seq[ThreadDto]
+
+ ThreadCreatedArgs* = ref object of Args
+ chatId*: string
+ parentMessageId*: string
+ threads*: seq[ThreadDto]
+
+ ThreadMessagesLoadedArgs* = ref object of Args
+ chatId*: string
+ threadId*: string
+ messages*: seq[MessageDto]
+
PinnedMessagesLoadedArgs* = ref object of Args
chatId*: string
pinnedMessages*: seq[PinnedMessageDto]
@@ -184,8 +206,14 @@ QtObject:
walletAccountService: wallet_account_service.Service
networkService: network_service.Service
msgCursor: Table[string, MessageCursor]
+ threadMsgCursor: Table[string, MessageCursor]
pinnedMsgCursor: Table[string, MessageCursor]
numOfPinnedMessagesPerChat: Table[string, int] # [chat_id, num_of_pinned_messages]
+ chatThreadsParentIdsByChat: Table[string, HashSet[string]]
+ chatThreadsLoadedChats: HashSet[string]
+ chatThreadsLoadingChats: HashSet[string]
+
+ proc asyncLoadChatThreads*(self: Service, chatId: string)
proc delete*(self: Service)
proc newService*(
@@ -207,7 +235,53 @@ QtObject:
result.walletAccountService = walletAccountService
result.networkService = networkService
result.msgCursor = initTable[string, MessageCursor]()
+ result.threadMsgCursor = initTable[string, MessageCursor]()
result.pinnedMsgCursor = initTable[string, MessageCursor]()
+ result.chatThreadsParentIdsByChat = initTable[string, HashSet[string]]()
+ result.chatThreadsLoadedChats = initHashSet[string]()
+ result.chatThreadsLoadingChats = initHashSet[string]()
+
+ proc replaceChatThreadsCache(self: Service, chatId: string, threads: seq[ThreadDto]) =
+ var parentIds = initHashSet[string]()
+ for thread in threads:
+ if thread.parentMessageId.len > 0:
+ parentIds.incl(thread.parentMessageId)
+ self.chatThreadsParentIdsByChat[chatId] = parentIds
+
+ proc cacheCreatedThreads(self: Service, chatId: string, threads: seq[ThreadDto]) =
+ if not self.chatThreadsParentIdsByChat.hasKey(chatId):
+ self.chatThreadsParentIdsByChat[chatId] = initHashSet[string]()
+
+ for thread in threads:
+ if thread.parentMessageId.len > 0:
+ self.chatThreadsParentIdsByChat[chatId].incl(thread.parentMessageId)
+
+ proc clearChatThreadsCacheForChat(self: Service, chatId: string) =
+ self.chatThreadsParentIdsByChat.del(chatId)
+ self.chatThreadsLoadedChats.excl(chatId)
+ self.chatThreadsLoadingChats.excl(chatId)
+
+ proc loadChatThreadsIfNeeded*(self: Service, chatId: string) =
+ if chatId.len == 0:
+ return
+
+ if self.chatThreadsLoadedChats.contains(chatId):
+ return
+
+ if self.chatThreadsLoadingChats.contains(chatId):
+ return
+
+ self.chatThreadsLoadingChats.incl(chatId)
+ self.asyncLoadChatThreads(chatId)
+
+ proc chatHasThreadForParentMessage*(self: Service, chatId: string, parentMessageId: string): bool =
+ if chatId.len == 0 or parentMessageId.len == 0:
+ return false
+
+ if not self.chatThreadsParentIdsByChat.hasKey(chatId):
+ return false
+
+ return self.chatThreadsParentIdsByChat[chatId].contains(parentMessageId)
proc isChatCursorInitialized(self: Service, chatId: string): bool =
return self.msgCursor.hasKey(chatId)
@@ -219,8 +293,26 @@ QtObject:
proc resetAllMessageCursors*(self: Service) =
self.msgCursor = initTable[string, MessageCursor]()
+ self.threadMsgCursor = initTable[string, MessageCursor]()
self.pinnedMsgCursor = initTable[string, MessageCursor]()
+ proc getThreadMessageCursorKey(self: Service, chatId: string, threadId: string): string =
+ return chatId & ":" & threadId
+
+ proc initOrGetThreadMessageCursor(self: Service, chatId: string, threadId: string): MessageCursor =
+ let key = self.getThreadMessageCursorKey(chatId, threadId)
+ if not self.threadMsgCursor.hasKey(key):
+ self.threadMsgCursor[key] = initMessageCursor(value="", pending=false, mostRecent=false)
+ return self.threadMsgCursor[key]
+
+ proc resetThreadMessageCursorsForChat*(self: Service, chatId: string) =
+ var keys = newSeq[string]()
+ for key in self.threadMsgCursor.keys:
+ if key.startsWith(chatId & ":"):
+ keys.add(key)
+ for key in keys:
+ self.threadMsgCursor.del(key)
+
proc initOrGetMessageCursor(self: Service, chatId: string): MessageCursor =
if(not self.msgCursor.hasKey(chatId)):
self.msgCursor[chatId] = initMessageCursor(value="", pending=false, mostRecent=false)
@@ -281,6 +373,64 @@ QtObject:
self.threadpool.start(arg)
return true
+ proc asyncLoadChatThreads*(self: Service, chatId: string) =
+ if chatId.len == 0:
+ error "empty chat id", procName="asyncLoadChatThreads"
+ return
+
+ let arg = AsyncFetchChatThreadsTaskArg(
+ tptr: asyncFetchChatThreadsTask,
+ vptr: cast[uint](self.vptr),
+ slot: "onAsyncLoadChatThreads",
+ chatId: chatId,
+ )
+
+ self.threadpool.start(arg)
+
+ proc asyncLoadMoreMessagesForThread*(self: Service, chatId: string, threadId: string,
+ limit = MESSAGES_PER_PAGE): bool =
+ if chatId.len == 0 or threadId.len == 0:
+ error "empty chat id or thread id", procName="asyncLoadMoreMessagesForThread"
+ return false
+
+ let msgCursor = self.initOrGetThreadMessageCursor(chatId, threadId)
+ if msgCursor.isPending():
+ return true
+
+ if msgCursor.isMostRecent():
+ return false
+
+ let msgCursorValue = msgCursor.getValue()
+ msgCursor.setPending()
+
+ let arg = AsyncFetchChatMessagesTaskArg(
+ tptr: asyncFetchChatMessagesTask,
+ vptr: cast[uint](self.vptr),
+ slot: "onAsyncLoadMoreMessagesForThread",
+ chatId: chatId,
+ threadId: threadId,
+ msgCursor: msgCursorValue,
+ limit: if(limit <= MESSAGES_PER_PAGE_MAX): limit else: MESSAGES_PER_PAGE_MAX,
+ )
+
+ self.threadpool.start(arg)
+ return true
+
+ proc asyncCreateThread*(self: Service, chatId: string, parentMessageId: string) =
+ if chatId.len == 0 or parentMessageId.len == 0:
+ error "empty chat id or parent message id", procName="asyncCreateThread"
+ return
+
+ let arg = AsyncCreateThreadTaskArg(
+ tptr: asyncCreateThreadTask,
+ vptr: cast[uint](self.vptr),
+ slot: "onAsyncCreateThread",
+ chatId: chatId,
+ parentMessageId: parentMessageId,
+ )
+
+ self.threadpool.start(arg)
+
proc onAsyncLoadReactionsForMessage*(self: Service, response: string) {.slot.} =
try:
let responseObj = response.parseJson
@@ -475,6 +625,13 @@ QtObject:
for k in keys:
self.msgCursor.del(k)
+ keys = @[]
+ for k in self.threadMsgCursor.keys:
+ if k.startsWith(communityId):
+ keys.add(k)
+ for k in keys:
+ self.threadMsgCursor.del(k)
+
keys = @[]
for k in self.pinnedMsgCursor.keys:
if k.startsWith(communityId):
@@ -482,6 +639,13 @@ QtObject:
for k in keys:
self.pinnedMsgCursor.del(k)
+ keys = @[]
+ for k in self.chatThreadsParentIdsByChat.keys:
+ if k.startsWith(communityId):
+ keys.add(k)
+ for k in keys:
+ self.clearChatThreadsCacheForChat(k)
+
self.events.emit(SIGNAL_RELOAD_MESSAGES, ReloadMessagesArgs(communityId: communityId))
proc init*(self: Service) =
@@ -550,6 +714,8 @@ QtObject:
self.events.on(SIGNAL_CHAT_LEFT) do(e: Args):
var chatArg = ChatArgs(e)
self.resetMessageCursor(chatArg.chatId)
+ self.resetThreadMessageCursorsForChat(chatArg.chatId)
+ self.clearChatThreadsCacheForChat(chatArg.chatId)
self.events.on(SignalType.LocalMessageBackupDone.event) do(e: Args):
self.resetAllMessageCursors()
@@ -678,6 +844,106 @@ QtObject:
# notify view, this is important
self.events.emit(SIGNAL_MESSAGES_LOADED, MessagesLoadedArgs())
+ proc onAsyncLoadChatThreads*(self: Service, response: string) {.slot.} =
+ var chatId = ""
+ try:
+ let responseObj = response.parseJson
+ if responseObj.kind != JObject:
+ raise newException(CatchableError, "load chat threads response is not a json object")
+
+ discard responseObj.getProp("chatId", chatId)
+
+ let errorString = responseObj{"error"}.getStr()
+ if errorString != "":
+ raise newException(CatchableError, errorString)
+
+ var threads: seq[ThreadDto]
+ var threadsArr: JsonNode
+ if responseObj.getProp("threads", threadsArr):
+ threads = map(threadsArr.getElems(), proc(x: JsonNode): ThreadDto = x.toThreadDto())
+
+ self.replaceChatThreadsCache(chatId, threads)
+ self.chatThreadsLoadedChats.incl(chatId)
+ self.chatThreadsLoadingChats.excl(chatId)
+
+ self.events.emit(SIGNAL_CHAT_THREADS_LOADED, ChatThreadsLoadedArgs(chatId: chatId, threads: threads))
+ except Exception as e:
+ if chatId.len > 0:
+ self.chatThreadsLoadingChats.excl(chatId)
+ self.events.emit(SIGNAL_CHAT_THREADS_LOADING_FAILED, ChatThreadsLoadedArgs(chatId: chatId, threads: @[]))
+ error "error loading chat threads", msg = e.msg
+
+ proc onAsyncLoadMoreMessagesForThread*(self: Service, response: string) {.slot.} =
+ var threadId: string = ""
+ var chatId: string = ""
+ try:
+ let responseObj = response.parseJson
+ if responseObj.kind != JObject:
+ raise newException(CatchableError, "load thread messages response is not a json object")
+
+ discard responseObj.getProp("chatId", chatId)
+ discard responseObj.getProp("threadId", threadId)
+
+ let errorString = responseObj{"error"}.getStr()
+ if errorString != "":
+ raise newException(CatchableError, errorString)
+
+ let msgCursor = self.initOrGetThreadMessageCursor(chatId, threadId)
+
+ var msgCursorValue: string
+ if responseObj.getProp("messagesCursor", msgCursorValue):
+ msgCursor.setValue(msgCursorValue)
+
+ var messagesArr: JsonNode
+ var messages: seq[MessageDto]
+ if responseObj.getProp("messages", messagesArr):
+ messages = map(messagesArr.getElems(), proc(x: JsonNode): MessageDto = x.toMessageDto())
+
+ self.checkPaymentRequestsInMessages(messages)
+
+ self.events.emit(SIGNAL_THREAD_MESSAGES_LOADED,
+ ThreadMessagesLoadedArgs(chatId: chatId, threadId: threadId, messages: messages))
+ except Exception as e:
+ error "error loading thread messages", msg = e.msg
+ if chatId.len > 0 and threadId.len > 0:
+ let key = self.getThreadMessageCursorKey(chatId, threadId)
+ if self.threadMsgCursor.hasKey(key):
+ # Clear the cursor for this thread so that we can try to load it again later
+ self.threadMsgCursor.del(key)
+ self.events.emit(SIGNAL_THREAD_MESSAGES_LOADING_FAILED,
+ ThreadMessagesLoadedArgs(chatId: chatId, threadId: threadId, messages: @[]))
+
+ proc onAsyncCreateThread*(self: Service, response: string) {.slot.} =
+ var chatId: string = ""
+ var parentMessageId: string = ""
+ try:
+ let responseObj = response.parseJson
+ if responseObj.kind != JObject:
+ raise newException(CatchableError, "create thread response is not a json object")
+
+ discard responseObj.getProp("chatId", chatId)
+ discard responseObj.getProp("parentMessageId", parentMessageId)
+
+ let errorString = responseObj{"error"}.getStr()
+ if errorString != "":
+ raise newException(CatchableError, errorString)
+
+
+ var threads: seq[ThreadDto]
+ var threadsArr: JsonNode
+ if responseObj.getProp("threads", threadsArr):
+ threads = map(threadsArr.getElems(), proc(x: JsonNode): ThreadDto = x.toThreadDto())
+
+ self.cacheCreatedThreads(chatId, threads)
+
+ self.events.emit(SIGNAL_THREAD_CREATED,
+ ThreadCreatedArgs(chatId: chatId, parentMessageId: parentMessageId, threads: threads))
+ except Exception as e:
+ error "error creating thread", msg = e.msg
+ if chatId.len > 0:
+ self.events.emit(SIGNAL_THREAD_CREATION_FAILED,
+ ThreadCreatedArgs(chatId: chatId, parentMessageId: parentMessageId, threads: @[]))
+
proc onAsyncLoadCommunityMemberAllMessages*(self: Service, response: string) {.slot.} =
try:
let rpcResponseObj = response.parseJson
diff --git a/src/app_service/service/stickers/async_tasks.nim b/src/app_service/service/stickers/async_tasks.nim
index ccdbc738f5..5294a092e2 100644
--- a/src/app_service/service/stickers/async_tasks.nim
+++ b/src/app_service/service/stickers/async_tasks.nim
@@ -86,6 +86,7 @@ const asyncSendStickerTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.}
"You can see a nice sticker here!",
arg.replyTo,
ContentType.Sticker.int,
+ "",
arg.preferredUsername,
standardLinkPreviews = JsonNode(),
statusLinkPreviews = JsonNode(),
diff --git a/src/backend/chat.nim b/src/backend/chat.nim
index 2191dee582..c9761d487f 100644
--- a/src/backend/chat.nim
+++ b/src/backend/chat.nim
@@ -55,6 +55,7 @@ proc sendChatMessage*(
msg: string,
replyTo: string,
contentType: int,
+ threadId: string = "",
preferredUsername: string = "",
standardLinkPreviews: JsonNode,
statusLinkPreviews: JsonNode,
@@ -68,6 +69,7 @@ proc sendChatMessage*(
"chatId": chatId,
"text": msg,
"responseTo": replyTo,
+ "threadId": threadId,
"ensName": preferredUsername,
"sticker": {
"hash": stickerHash,
@@ -86,6 +88,7 @@ proc sendImages*(chatId: string,
msg: string,
replyTo: string,
preferredUsername: string,
+ threadId: string = "",
standardLinkPreviews: JsonNode,
statusLinkPreviews: JsonNode,
paymentRequests: JsonNode,
@@ -98,6 +101,7 @@ proc sendImages*(chatId: string,
"ensName": preferredUsername,
"text": msg,
"responseTo": replyTo,
+ "threadId": threadId,
"linkPreviews": standardLinkPreviews,
"statusLinkPreviews": statusLinkPreviews,
"paymentRequests": paymentRequests,
@@ -105,6 +109,14 @@ proc sendImages*(chatId: string,
)
callPrivateRPC("sendChatMessages".prefix, %* [imagesJson])
+proc createThread*(chatId: string, parentMessageId: string): RpcResponse[JsonNode] =
+ let payload = %* [chatId, parentMessageId]
+ result = callPrivateRPC("createThread".prefix, payload)
+
+proc fetchChatThreads*(chatId: string): RpcResponse[JsonNode] =
+ let payload = %* [chatId]
+ result = callPrivateRPC("chatThreads".prefix, payload)
+
proc muteChat*(chatId: string, interval: int): RpcResponse[JsonNode] =
result = callPrivateRPC("muteChatV2".prefix, %* [
{
diff --git a/src/backend/messages.nim b/src/backend/messages.nim
index a9a41d44a2..28f75a6d59 100644
--- a/src/backend/messages.nim
+++ b/src/backend/messages.nim
@@ -4,9 +4,9 @@ import response_type
export response_type
-proc fetchMessages*(chatId: string, cursorVal: string, limit: int): RpcResponse[JsonNode] =
- let payload = %* [chatId, cursorVal, limit]
- result = callPrivateRPC("chatMessages".prefix, payload)
+proc fetchMessages*(chatId: string, threadId: string = "", cursorVal: string, limit: int): RpcResponse[JsonNode] =
+ let payload = %* [chatId, threadId, cursorVal, limit]
+ result = callPrivateRPC("chatMessagesV2".prefix, payload)
proc fetchPinnedMessages*(chatId: string, cursorVal: string, limit: int): RpcResponse[JsonNode] =
let payload = %* [chatId, cursorVal, limit]
diff --git a/ui/app/AppLayouts/Chat/ChatLayout.qml b/ui/app/AppLayouts/Chat/ChatLayout.qml
index 4ab125cc6d..0e1c238ded 100644
--- a/ui/app/AppLayouts/Chat/ChatLayout.qml
+++ b/ui/app/AppLayouts/Chat/ChatLayout.qml
@@ -99,6 +99,7 @@ StackLayout {
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
property string disabledTooltipText
property int extraLeftPadding: 0
@@ -317,6 +318,7 @@ StackLayout {
root.communityPermissionsStore.viewOnlyPermissionsModel.count > 0
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
messageLinkSharingEnabled: root.messageLinkSharingEnabled
+ threadsFeatureEnabled: root.threadsFeatureEnabled
disabledTooltipText: root.disabledTooltipText
paymentRequestFeatureEnabled: root.paymentRequestFeatureEnabled
extraLeftPadding: root.extraLeftPadding
diff --git a/ui/app/AppLayouts/Chat/stores/MessageStore.qml b/ui/app/AppLayouts/Chat/stores/MessageStore.qml
index 51923349bd..059d049c3c 100644
--- a/ui/app/AppLayouts/Chat/stores/MessageStore.qml
+++ b/ui/app/AppLayouts/Chat/stores/MessageStore.qml
@@ -23,6 +23,7 @@ QtObject {
readonly property string chatColor: messageModule ? messageModule.chatColor : Theme.palette.primaryColor1
readonly property string chatIcon: messageModule ? messageModule.chatIcon : ""
readonly property bool keepUnread: messageModule ? messageModule.keepUnread : false
+ readonly property string threadId: messageModule ? messageModule.threadId : ""
onMessageModuleChanged: {
if(!messageModule)
@@ -232,4 +233,10 @@ QtObject {
return sharedUrlsModule.createMessageUrl(chatId, messageId)
}
+
+ function createThread(parentMessageId) {
+ if (!messageModule)
+ return
+ messageModule.createThread(parentMessageId)
+ }
}
diff --git a/ui/app/AppLayouts/Chat/stores/RootStore.qml b/ui/app/AppLayouts/Chat/stores/RootStore.qml
index 7b1c2028ca..2a23d0b502 100644
--- a/ui/app/AppLayouts/Chat/stores/RootStore.qml
+++ b/ui/app/AppLayouts/Chat/stores/RootStore.qml
@@ -294,7 +294,10 @@ QtObject {
return UrlUtils.convertUrlToLocalPath(file)
}
})
- chatContentModule.inputAreaModule.sendImages(JSON.stringify(convertedImagePaths), textMsg.trim(), replyMessageId)
+ chatContentModule.inputAreaModule.sendImages(
+ JSON.stringify(convertedImagePaths),
+ textMsg.trim(),
+ replyMessageId)
result = true
} else {
if (textMsg.trim() !== "") {
diff --git a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml
index 411ced0a9b..011f118661 100644
--- a/ui/app/AppLayouts/Chat/views/ChatColumnView.qml
+++ b/ui/app/AppLayouts/Chat/views/ChatColumnView.qml
@@ -68,6 +68,7 @@ Item {
property bool amIBanned: false
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
property string disabledTooltipText
property bool paymentRequestFeatureEnabled
property bool joined
@@ -490,6 +491,7 @@ Item {
isBlocked: model.blocked
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
messageLinkSharingEnabled: root.messageLinkSharingEnabled
+ threadsFeatureEnabled: root.threadsFeatureEnabled
disabledTooltipText: root.disabledTooltipText
areTestNetworksEnabled: root.areTestNetworksEnabled
extraLeftPadding: root.extraLeftPadding
@@ -512,6 +514,13 @@ Item {
onEditMessageRequested: (messageId) => {
d.startEditMessage(messageId)
}
+ onOpenThread: (messageId) => {
+ if (root.threadsFeatureEnabled
+ && root.activeChatType === Constants.chatType.communityChat
+ && !d.activeMessagesStore.threadId) {
+ d.activeMessagesStore.createThread(messageId)
+ }
+ }
onForceInputFocus: {
chatInput.forceInputActiveFocus()
}
@@ -668,7 +677,7 @@ Item {
return
}
- if (root.rootStore.sendMessage(d.activeChatContentModule.getMyChatId(),
+ if (root.rootStore.sendMessage(root.activeChatId,
chatInput.getTextWithPublicKeys(),
chatInput.isReply? chatInput.replyMessageId : "",
chatInput.fileUrlsAndSources
@@ -729,4 +738,4 @@ Item {
}
}
}
-}
+}
\ No newline at end of file
diff --git a/ui/app/AppLayouts/Chat/views/ChatContentView.qml b/ui/app/AppLayouts/Chat/views/ChatContentView.qml
index bf1a91eb4d..64c0f157ef 100644
--- a/ui/app/AppLayouts/Chat/views/ChatContentView.qml
+++ b/ui/app/AppLayouts/Chat/views/ChatContentView.qml
@@ -60,6 +60,7 @@ ColumnLayout {
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
property string disabledTooltipText
property int extraLeftPadding: 0
@@ -68,6 +69,7 @@ ColumnLayout {
property string myPublicKey
signal showReplyArea(messageId: string)
+ signal openThread(messageId: string)
signal forceInputFocus()
signal editMessageRequested(messageId: string)
@@ -140,6 +142,7 @@ ColumnLayout {
channelEmoji: !chatContentModule ? "" : (chatContentModule.chatDetails.emoji || "")
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
messageLinkSharingEnabled: root.messageLinkSharingEnabled
+ threadsFeatureEnabled: root.threadsFeatureEnabled
disabledTooltipText: root.disabledTooltipText
areTestNetworksEnabled: root.areTestNetworksEnabled
extraLeftPadding: root.extraLeftPadding
@@ -156,6 +159,9 @@ ColumnLayout {
onShowReplyArea: (messageId, senderId) => {
root.showReplyArea(messageId)
}
+ onOpenThread: (messageId) => {
+ root.openThread(messageId)
+ }
onOpenStickerPackPopup: stickerPackId => root.openStickerPackPopup(stickerPackId)
onTokenPaymentRequested: root.tokenPaymentRequested(recipientAddress, tokenKey, rawAmount)
onEditModeChanged: (editModeOn, messageId) => {
diff --git a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml
index 3b45fc9651..e539eb1245 100644
--- a/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml
+++ b/ui/app/AppLayouts/Chat/views/ChatMessagesView.qml
@@ -66,6 +66,7 @@ Item {
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
property string disabledTooltipText
property int extraLeftPadding: 0
@@ -78,6 +79,7 @@ Item {
signal tokenPaymentRequested(string recipientAddress, string tokenKey, string rawAmount)
signal showReplyArea(string messageId, string author)
signal editModeChanged(bool editModeOn, string messageId)
+ signal openThread(string messageId)
// Unfurling related requests:
signal setNeverAskAboutUnfurlingAgain(bool neverAskAgain)
@@ -360,6 +362,7 @@ Item {
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
messageLinkSharingEnabled: root.messageLinkSharingEnabled
createMessageLink: (chatId, messageId) => root.messageStore.createMessageLink(chatId, messageId)
+ threadsFeatureEnabled: root.threadsFeatureEnabled
disabledTooltipText: root.disabledTooltipText
areTestNetworksEnabled: root.areTestNetworksEnabled
extraLeftPadding: root.extraLeftPadding
@@ -418,6 +421,7 @@ Item {
quotedMessageAlbumMessageImages: model.quotedMessageAlbumMessageImages.split(" ")
quotedMessageAlbumImagesCount: model.quotedMessageAlbumImagesCount
bridgeName: model.bridgeName
+ hasThread: model.hasThread
gapFrom: model.gapFrom
gapTo: model.gapTo
@@ -446,6 +450,7 @@ Item {
onTokenPaymentRequested: root.tokenPaymentRequested(recipientAddress, tokenKey, rawAmount)
onShowReplyArea: (messageId, author) => root.showReplyArea(messageId, author)
+ onOpenThread: (messageId) => root.openThread(messageId)
stickersLoaded: root.stickersLoaded
diff --git a/ui/app/AppLayouts/Chat/views/ChatView.qml b/ui/app/AppLayouts/Chat/views/ChatView.qml
index 4eaa5284d4..1c5f77c237 100644
--- a/ui/app/AppLayouts/Chat/views/ChatView.qml
+++ b/ui/app/AppLayouts/Chat/views/ChatView.qml
@@ -110,6 +110,7 @@ Item {
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
property string disabledTooltipText
property bool paymentRequestFeatureEnabled
@@ -496,6 +497,7 @@ Item {
amIBanned: root.sectionItemModel ? root.sectionItemModel.amIBanned : false
sendViaPersonalChatEnabled: root.sendViaPersonalChatEnabled
messageLinkSharingEnabled: root.messageLinkSharingEnabled
+ threadsFeatureEnabled: root.threadsFeatureEnabled
disabledTooltipText: root.disabledTooltipText
paymentRequestFeatureEnabled: root.paymentRequestFeatureEnabled
extraLeftPadding: root.extraLeftPadding
diff --git a/ui/app/AppLayouts/stores/FeatureFlagsStore.qml b/ui/app/AppLayouts/stores/FeatureFlagsStore.qml
index 28ebb1bc19..8c6443514f 100644
--- a/ui/app/AppLayouts/stores/FeatureFlagsStore.qml
+++ b/ui/app/AppLayouts/stores/FeatureFlagsStore.qml
@@ -15,4 +15,5 @@ QtObject {
property bool messageLinkSharingEnabled
property bool statusSupportBotEnabled
property bool buyEnabled
+ property bool threadsEnabled
}
diff --git a/ui/app/mainui/sectionLoaders/ChatLoader.qml b/ui/app/mainui/sectionLoaders/ChatLoader.qml
index 333f4b8fe6..aa7200669a 100644
--- a/ui/app/mainui/sectionLoaders/ChatLoader.qml
+++ b/ui/app/mainui/sectionLoaders/ChatLoader.qml
@@ -248,6 +248,7 @@ Loader {
? root.networkConnectionStore.walletReadyForTransactionsToolTipText : ""),
messageLinkSharingEnabled: Qt.binding(() => root.featureFlagsStore.messageLinkSharingEnabled
&& root.advancedStore.copyMessageLinksEnabled),
+ threadsFeatureEnabled: Qt.binding(() => root.featureFlagsStore.threadsEnabled),
paymentRequestFeatureEnabled: Qt.binding(() => root.featureFlagsStore.paymentRequestEnabled),
extraLeftPadding: Qt.binding(() => root.isPortraitMode ? SQUtils.Utils.swipeIndicatorWidth : 0),
isPortraitMode: Qt.binding(() => root.isPortraitMode),
diff --git a/ui/app/mainui/sectionLoaders/CommunityChatLoader.qml b/ui/app/mainui/sectionLoaders/CommunityChatLoader.qml
index 99914f8871..6c7f8ae8e4 100644
--- a/ui/app/mainui/sectionLoaders/CommunityChatLoader.qml
+++ b/ui/app/mainui/sectionLoaders/CommunityChatLoader.qml
@@ -318,6 +318,7 @@ Loader {
advancedStore: Qt.binding(() => root.advancedStore),
messageLinkSharingEnabled: Qt.binding(() => root.featureFlagsStore.messageLinkSharingEnabled
&& root.advancedStore.copyMessageLinksEnabled),
+ threadsFeatureEnabled: Qt.binding(() => root.featureFlagsStore.threadsEnabled),
paymentRequestFeatureEnabled: Qt.binding(() => root.featureFlagsStore.paymentRequestEnabled),
extraLeftPadding: Qt.binding(() => root.isPortraitMode ? SQUtils.Utils.swipeIndicatorWidth : 0),
mutualContactsModel: Qt.binding(() => root.contactsAdaptor.mutualContacts),
diff --git a/ui/i18n/qml_base_en.ts b/ui/i18n/qml_base_en.ts
index 9011a17175..29b5071bd1 100644
--- a/ui/i18n/qml_base_en.ts
+++ b/ui/i18n/qml_base_en.ts
@@ -11356,6 +11356,14 @@ to load
Reply to
+
+ Open Thread
+
+
+
+ Create Thread
+
+
Edit message
diff --git a/ui/i18n/qml_cs.ts b/ui/i18n/qml_cs.ts
index 220bea95a1..a47b836b5e 100644
--- a/ui/i18n/qml_cs.ts
+++ b/ui/i18n/qml_cs.ts
@@ -11426,6 +11426,14 @@ selhalo
Reply to
Odpovědět na
+
+ Open Thread
+
+
+
+ Create Thread
+
+
Edit message
Upravit zprávu
diff --git a/ui/i18n/qml_es.ts b/ui/i18n/qml_es.ts
index 3b92750251..5e2d2f5bf6 100644
--- a/ui/i18n/qml_es.ts
+++ b/ui/i18n/qml_es.ts
@@ -11369,6 +11369,14 @@ al cargar
Reply to
Responder a
+
+ Open Thread
+
+
+
+ Create Thread
+
+
Edit message
Editar mensaje
diff --git a/ui/i18n/qml_fr.ts b/ui/i18n/qml_fr.ts
index 4e619413b5..b48bd94e69 100644
--- a/ui/i18n/qml_fr.ts
+++ b/ui/i18n/qml_fr.ts
@@ -11366,6 +11366,14 @@ chargement
Reply to
Répondre à
+
+ Open Thread
+ Ouvrir le fil
+
+
+ Create Thread
+ Créer un fil
+
Edit message
Modifier le message
@@ -17019,11 +17027,11 @@ avec un retour à la ligne
Fastest
-
+ Le plus rapide
Lowest fee
-
+ Frais le plus bas
Swap + Bridge
@@ -17040,11 +17048,11 @@ avec un retour à la ligne
%1s
short for seconds
- %1 s
+ %1 s
Choose route
-
+ Choisir la route
by %1
diff --git a/ui/i18n/qml_ko.ts b/ui/i18n/qml_ko.ts
index 79a9c71cd0..41557d2582 100644
--- a/ui/i18n/qml_ko.ts
+++ b/ui/i18n/qml_ko.ts
@@ -11313,6 +11313,14 @@ to load
Reply to
답글 대상
+
+ Open Thread
+
+
+
+ Create Thread
+
+
Edit message
메시지 편집
diff --git a/ui/i18n/qml_uk.ts b/ui/i18n/qml_uk.ts
index 28ffc9d8bd..85866293a0 100644
--- a/ui/i18n/qml_uk.ts
+++ b/ui/i18n/qml_uk.ts
@@ -1842,6 +1842,10 @@ from "%1" to "%2"
This channel no longer exists
Цей канал більше не існує
+
+ Send the Status Team bot a contact request to get useful Status tips, important updates, and share your feedback, ideas, or issues directly with the Status team.
+
+
Invite People
Запросити людей
@@ -11425,6 +11429,14 @@ to load
Reply to
Відповісти
+
+ Open Thread
+
+
+
+ Create Thread
+
+
Edit message
Редагувати
@@ -13667,6 +13679,10 @@ to load
PrimaryNavSidebar
+
+ Status Help Bot
+
+
Activity Center
Центр активності
@@ -18682,6 +18698,10 @@ If a transaction with a lower nonce is pending, higher nonce transactions will r
Copy link to profile
Копіювати посилання на профіль
+
+ Settings
+ Налаштування
+
Always online
Завжди в мережі
diff --git a/ui/imports/shared/views/chat/MessageContextMenuView.qml b/ui/imports/shared/views/chat/MessageContextMenuView.qml
index 0ee2960b04..f8dee56d35 100644
--- a/ui/imports/shared/views/chat/MessageContextMenuView.qml
+++ b/ui/imports/shared/views/chat/MessageContextMenuView.qml
@@ -28,6 +28,8 @@ StatusMenu {
property string selectedText
property bool pinMessageAllowedForMembers: false
+ property bool threadsFeatureEnabled: false
+ property bool hasThread: false
property bool isDebugEnabled: false
property bool editRestricted: false
property bool pinnedMessage: false
@@ -43,6 +45,7 @@ StatusMenu {
signal unpinMessage()
signal pinnedMessagesLimitReached()
signal showReplyArea(string messageSenderId)
+ signal openThread()
signal toggleReaction(string hexcode)
signal deleteMessage()
signal editClicked()
@@ -87,6 +90,17 @@ StatusMenu {
enabled: !root.disabledForChat
}
+ StatusAction {
+ id: openThreadAction
+ objectName: "messageContextMenu_openThread"
+ text: root.hasThread ? qsTr("Open Thread") : qsTr("Create Thread")
+ icon.name: "chat"
+ onTriggered: root.openThread()
+ enabled: !root.disabledForChat &&
+ root.threadsFeatureEnabled &&
+ root.chatType === Constants.chatType.communityChat
+ }
+
StatusAction {
id: editMessageAction
objectName: "messageContextMenu_edit"
@@ -178,6 +192,7 @@ StatusMenu {
StatusMenuSeparator {
visible: deleteMessageAction.enabled &&
(replyToMenuItem.enabled ||
+ openThreadAction.enabled ||
copyMessageMenuItem.enabled ||
copyMessageIdAction.enabled ||
copyMessageLinkAction.enabled ||
diff --git a/ui/imports/shared/views/chat/MessageView.qml b/ui/imports/shared/views/chat/MessageView.qml
index 889ac2fa7f..cc8b42fbe6 100644
--- a/ui/imports/shared/views/chat/MessageView.qml
+++ b/ui/imports/shared/views/chat/MessageView.qml
@@ -149,6 +149,8 @@ Loader {
property bool sendViaPersonalChatEnabled
property bool messageLinkSharingEnabled
+ property bool threadsFeatureEnabled
+ property bool hasThread: false
property string disabledTooltipText
property int extraLeftPadding: 0
@@ -274,6 +276,8 @@ Loader {
myPublicKey: userProfile.pubKey,
amIChatAdmin: root.amIChatAdmin,
pinMessageAllowedForMembers: messageStore.isPinMessageAllowedForMembers,
+ threadsFeatureEnabled: root.threadsFeatureEnabled,
+ hasThread: root.hasThread,
chatType: messageStore.chatType,
messageId: root.messageId,
@@ -328,6 +332,7 @@ Loader {
}
signal showReplyArea(string messageId, string author)
+ signal openThread(string messageId)
function startMessageFoundAnimation() {
@@ -1352,6 +1357,9 @@ Loader {
onShowReplyArea: (senderId) => {
root.showReplyArea(messageContextMenuView.messageId, senderId)
}
+ onOpenThread: {
+ root.openThread(messageContextMenuView.messageId)
+ }
onCopyToClipboard: (text) => {
ClipboardUtils.setText(text)
}
diff --git a/ui/main.qml b/ui/main.qml
index 444c5a7fa4..5bafc7979d 100644
--- a/ui/main.qml
+++ b/ui/main.qml
@@ -55,6 +55,7 @@ Window {
messageLinkSharingEnabled: featureFlags ? featureFlags.messageLinkSharingEnabled : false
statusSupportBotEnabled: featureFlags ? featureFlags.statusSupportBotEnabled : false
buyEnabled: featureFlags ? featureFlags.buyEnabled : false
+ threadsEnabled: featureFlags ? featureFlags.threadsEnabled : false
}
readonly property UtilsStore utilsStore: UtilsStore {}
diff --git a/vendor/status-go b/vendor/status-go
index d39761cb7a..563b40c238 160000
--- a/vendor/status-go
+++ b/vendor/status-go
@@ -1 +1 @@
-Subproject commit d39761cb7afb3aa23308d2f73e89770c3d4007e1
+Subproject commit 563b40c238e7126ef25c59dbc16e39249354c7ab