mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 07:01:14 +00:00
feat(threads): implement threads feature POC
Fixes #21257 Implements threaded messages in community channels with UI integration, gated behind a feature flag ### Features - **Create thread** — right-click any message → "Create Thread" opens a thread rooted at that message as a sub-chat - **Open thread** — if a thread already exists the action shows "Open Thread" instead - **Auto-display threads** — threads are automatically listed in chat when loaded, positioned directly below their parent chat - **Parallel to replies** — threads are independent of the existing reply feature ### Architecture - Reuses `sendMessage` with a `threadId` parameter; no duplicate send variants - Thread messages are lazy-loaded per chat on first open and cached in memory - `hasThread` model role derived from the cache drives the Open/Create label - `threadId == parentMessageId` (validated against status-go behaviour), so no separate ID mapping is needed - Unified `MessageByChatID` API across all layers (Go → Nim → RPC) with optional `threadId` parameter - Thread message routing fixed: base chat only shows messages with `threadId IS NULL OR threadId = ''` - `isThread` property on ChatItem enables proper list positioning and future UI styling ### Implementation Details - Go layer: `MessageByChatID(chatID, threadID, ...)` handles both base and thread message fetching - Nim layer: Single `fetchMessages` function with optional threadId parameter - UI layer: Threads auto-populate in chat list via `SIGNAL_CHAT_THREADS_LOADED` signal - Threading: No separate reactions fetch for thread messages (only base chat reactions)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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.} =
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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) =
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.} =
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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, %* [
|
||||
{
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() !== "") {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,4 +15,5 @@ QtObject {
|
||||
property bool messageLinkSharingEnabled
|
||||
property bool statusSupportBotEnabled
|
||||
property bool buyEnabled
|
||||
property bool threadsEnabled
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -11356,6 +11356,14 @@ to load</source>
|
||||
<source>Reply to</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation type="unfinished"></translation>
|
||||
|
||||
@@ -11426,6 +11426,14 @@ selhalo</translation>
|
||||
<source>Reply to</source>
|
||||
<translation>Odpovědět na</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation>Upravit zprávu</translation>
|
||||
|
||||
@@ -11369,6 +11369,14 @@ al cargar</translation>
|
||||
<source>Reply to</source>
|
||||
<translation>Responder a</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation>Editar mensaje</translation>
|
||||
|
||||
+12
-4
@@ -11366,6 +11366,14 @@ chargement</translation>
|
||||
<source>Reply to</source>
|
||||
<translation>Répondre à</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation>Ouvrir le fil</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation>Créer un fil</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation>Modifier le message</translation>
|
||||
@@ -17019,11 +17027,11 @@ avec un retour à la ligne</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Fastest</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Le plus rapide</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Lowest fee</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Frais le plus bas</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Swap + Bridge</source>
|
||||
@@ -17040,11 +17048,11 @@ avec un retour à la ligne</translation>
|
||||
<message>
|
||||
<source>%1s</source>
|
||||
<comment>short for seconds</comment>
|
||||
<translation type="unfinished">%1 s</translation>
|
||||
<translation>%1 s</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Choose route</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation>Choisir la route</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>by %1</source>
|
||||
|
||||
@@ -11313,6 +11313,14 @@ to load</source>
|
||||
<source>Reply to</source>
|
||||
<translation>답글 대상</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation>메시지 편집</translation>
|
||||
|
||||
@@ -1842,6 +1842,10 @@ from "%1" to "%2"</source>
|
||||
<source>This channel no longer exists</source>
|
||||
<translation>Цей канал більше не існує</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>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.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Invite People</source>
|
||||
<translation>Запросити людей</translation>
|
||||
@@ -11425,6 +11429,14 @@ to load</source>
|
||||
<source>Reply to</source>
|
||||
<translation>Відповісти</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Create Thread</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit message</source>
|
||||
<translation>Редагувати</translation>
|
||||
@@ -13667,6 +13679,10 @@ to load</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>PrimaryNavSidebar</name>
|
||||
<message>
|
||||
<source>Status Help Bot</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Activity Center</source>
|
||||
<translation>Центр активності</translation>
|
||||
@@ -18682,6 +18698,10 @@ If a transaction with a lower nonce is pending, higher nonce transactions will r
|
||||
<source>Copy link to profile</source>
|
||||
<translation>Копіювати посилання на профіль</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Settings</source>
|
||||
<translation type="unfinished">Налаштування</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Always online</source>
|
||||
<translation>Завжди в мережі</translation>
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Vendored
+1
-1
Submodule vendor/status-go updated: d39761cb7a...563b40c238
Reference in New Issue
Block a user