mirror of
https://github.com/status-im/status-app.git
synced 2026-08-27 07:01:14 +00:00
* fix(media): re-point cached media URLs when the media server restarts (#47) iOS suspends the app and kills status-go's local media server; on resume it rebinds on a NEW ephemeral port and emits mediaserver.started, which the Nim side never handled — every cached https://localhost:<oldport>/ image URL (chat images, avatars, stickers, link previews, community icons) went permanently stale. Key decisions: - New MediaServerStartedSignal decoded in signals_manager (the enum member already existed, so the cheap-triage scan needs no change); null-event envelopes decode to port 0 and every rewrite helper treats port <= 0 as a no-op. - One shared helper, withMediaServerPort (app_service/common/ media_server_url.nim): rewrites the port ONLY for http(s):// localhost|127.0.0.1|0.0.0.0:<port> URLs, splicing around the authority so path/query stay byte-identical; everything else (remote URLs, data URIs, qrc/file paths, port-less URLs) passes through untouched, so callers apply it blindly. Desktop stays a no-op: the signal never fires there, and an unchanged port rewrites nothing. - Subscribers rewrite in place + notify: message models (chat + pinned) re-emit image-carrying roles via dataChanged; link-preview thumbnails are QObjects and emit urlChanged directly; section model refreshes community image/banner/icon; contacts service rewrites its ContactDetails cache and re-emits SIGNAL_CONTACT_UPDATED per changed contact, plus the user-profile singleton's own avatar (its setters already no-op on equal values). - Dropped the contacts service's imageServerUrl field: it was write-only dead code (fetched once, never read). Files: src/app/core/signals/{signals_manager,types}.nim, src/app/core/signals/remote_signals/mediaserver.nim (new), src/app_service/common/media_server_url.nim (new), src/app_service/service/contacts/service.nim, src/app_service/service/message/dto/{link_preview,link_preview_thumbnail}.nim, src/app/modules/shared_models/{message_item,message_model,link_preview_model,section_model}.nim, chat_section chat_content + messages + main module/controller/io_interface wiring, test/nim/{media_server_url_test (new),message_model_test, signals_manager_test}.nim Verified: media_server_url_test 9/9 OK, signals_manager_test 6/6 OK (incl. 2 new mediaserver.started tests) via make nim-test-run USE_SYSTEM_NIM=1. message_model_test does NOT link in this arm64 container — pre-existing nimqml/LTO link failure, reproduced identically on clean HEAD with a fresh nimcache; the 3 new model tests compile but need CI/another host to run. Device repro (background 10+ min, resume, images reload) remains for the human pass. * fix(build): normalize two 'import Nimqml' casings to the module's real name discord_message_item and message_transaction_parameters_item imported Nimqml (capital N) while the vendored module file is nimqml.nim. On a case-insensitive checkout (Docker-on-Mac bind mount) Nim treats the two spellings as distinct modules whose nimcache artifacts collide on one file, so any test pulling in these items (e.g. message_model_test) fails to link with undefined nimqml symbols. Found while verifying #47. * refactor(media): deduplicate the media-URL refresh template and drop dead code Review follow-up to ca5e5c92c, no behavior change: - Extract the thrice-duplicated compare-rewrite-flag template into refreshMediaServerUrl in media_server_url.nim; message_item, section_model and contacts/service now share one definition. - Drop the why-comment copy-pasted verbatim into three controllers; the explanation lives on MediaServerStartedSignal and the helper module. - Remove backend getImageServerURL, dead since its only caller (setImageServerUrl) was deleted on this branch. - Whitespace: trailing newline in signals/types.nim, blank line before the appended suite in message_model_test.nim. Verified: media_server_url_test, message_model_test and signals_manager_test all pass (nim-test-run recipe); app wiring compile-checked via app/modules/main/module + contacts service + section_model with --compileOnly. * fix(ios): drive PauseServices/ResumeServices from the app lifecycle (#51) On iOS status-go runs in-process and nothing drove the pausable-services lifecycle: services never paused on backgrounding (battery cost) and never resumed on foregrounding, so the media server's listening socket iOS kills during suspension stayed dead and every cached localhost media URL failed until app restart (device-confirmed: post-resume image loads get Connection refused while in-process RPC still works). Resuming now re-runs the full recovery chain: ResumeServices -> ServiceRegistry -> mediaserver ToForeground() rebind -> mediaserver.started -> #47/#49's URL refresh. Key decisions: - Lifecycle source is StatusQ's UrlSchemeEvent applicationStateChanged watcher (already the iOS foreground seam): new appBackgrounded signal emitted ONLY on Qt::ApplicationSuspended — Inactive dips from share sheets/system alerts never pause; appForegrounded fires on every return to Active, so ServicesPauseBridge latches (`paused`) and resumes only when it actually paused. Connections are synchronous (AutoConnection, same thread): iOS freezes the process right after the state change, a queued pause slot might never run. - Bridge wired only under `when defined(ios)`: Android's service process already drives pause/resume from binder visibility (UI process must not double-drive), desktop is never suspended. - Same contract as StatusGoService.java: fetch the service list from PausableServices() at each transition (late-registered services picked up; empty list — node not running, e.g. login screen — drives nothing), then PauseServices/ResumeServices with the JSON name array. The three libstatus C exports are bound in new backend/pausable_services.nim (vendor/nim-status-go doesn't cover them); errors logged, not fatal. - Backend calls injected into the bridge as a seam (PausableServicesCalls), so the Nim test drives the real StatusQ signal emitters against recorders. Files: src/app/core/services_pause_bridge.nim (new), src/backend/pausable_services.nim (new), src/nim_status_client.nim, src/statusq_bridge.nim, ui/StatusQ/{include/StatusQ/urlschemeevent.h, src/{urlschemeevent,externc}.cpp}, Makefile (test target), test/nim/services_pause_bridge_test.nim (new) Verified: services_pause_bridge_test 15/15 OK; full tests-nim-linux 464 OK / 0 failed (chat_section_model_test fails to compile identically on the clean base — pre-existing, unrelated); nim_status_client Linux path compile-checked (--compileOnly, exit 0); StatusQ recompiles clean; libstatus.so exports PausableServices/PauseServices/ResumeServices (linked by the test). Device criteria (iPhone: photo -> background 10+ min -> resume -> images recover; Android S21: exactly one service-driven pause/resume, no duplicates) remain for the human pass — iOS paths can't run in this container. * fix(tests): reset the fetch counter per case in services_pause_bridge unittest setup vars are module-scope globals; the int's constant initializer runs once, so fetches leaked across cases and the two absolute-count assertions saw the running total. * fix: Potential fix for pull request finding * fix(statusq): handle the full application-state enum in watchApplicationState Inactive is an expected transient dip and stays a no-op; anything else unhandled now logs a qWarning. * fix(tests): empty pausable set no longer latches paused, so foregrounding skips the fetch
263 lines
10 KiB
Nim
263 lines
10 KiB
Nim
import tables
|
|
import io_interface
|
|
|
|
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/wallet_account/service as wallet_account_service
|
|
|
|
import ../../../../core/signals/types
|
|
import ../../../../core/eventemitter
|
|
import ../../../../core/unique_event_emitter
|
|
|
|
type
|
|
Controller* = ref object of RootObj
|
|
delegate: io_interface.AccessInterface
|
|
events: UniqueUUIDEventEmitter
|
|
sectionId: string
|
|
chatId: string
|
|
belongsToCommunity: bool
|
|
isUsersListAvailable: bool #users list is not available for 1:1 chat
|
|
nodeConfigurationService: node_configuration_service.Service
|
|
settingsService: settings_service.Service
|
|
mailserversService: mailservers_service.Service
|
|
contactService: contact_service.Service
|
|
chatService: chat_service.Service
|
|
communityService: community_service.Service
|
|
messageService: message_service.Service
|
|
|
|
# Forward declaration
|
|
proc getChatDetails*(self: Controller): lent ChatDto
|
|
|
|
proc newController*(delegate: io_interface.AccessInterface, events: EventEmitter, sectionId: string, chatId: string,
|
|
belongsToCommunity: bool, isUsersListAvailable: bool, 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): Controller =
|
|
result = Controller()
|
|
result.delegate = delegate
|
|
result.events = initUniqueUUIDEventEmitter(events)
|
|
result.sectionId = sectionId
|
|
result.chatId = chatId
|
|
result.belongsToCommunity = belongsToCommunity
|
|
result.isUsersListAvailable = isUsersListAvailable
|
|
result.settingsService = settingsService
|
|
result.nodeConfigurationService = nodeConfigurationService
|
|
result.contactService = contactService
|
|
result.chatService = chatService
|
|
result.communityService = communityService
|
|
result.messageService = messageService
|
|
|
|
proc delete*(self: Controller) =
|
|
self.events.disconnect()
|
|
|
|
proc init*(self: Controller) =
|
|
self.events.on(SignalType.MediaServerStarted.event) do(e: Args):
|
|
let args = MediaServerStartedSignal(e)
|
|
self.delegate.onMediaServerStarted(args.port)
|
|
|
|
self.events.on(SIGNAL_PINNED_MESSAGES_LOADED) do(e:Args):
|
|
let args = PinnedMessagesLoadedArgs(e)
|
|
if(self.chatId != args.chatId or args.pinnedMessages.len == 0):
|
|
return
|
|
self.delegate.newPinnedMessagesLoaded(args.pinnedMessages, args.reactions)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_PINNED) do(e:Args):
|
|
let args = MessagePinUnpinArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onPinMessage(args.messageId, args.actionInitiatedBy)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_UNPINNED) do(e:Args):
|
|
let args = MessagePinUnpinArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onUnpinMessage(args.messageId)
|
|
|
|
self.events.on(SIGNAL_CHAT_MUTED) do(e:Args):
|
|
let args = ChatArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onChatMuted()
|
|
|
|
self.events.on(SIGNAL_CHAT_UNMUTED) do(e:Args):
|
|
let args = ChatArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onChatUnmuted()
|
|
|
|
self.events.on(SIGNAL_MESSAGE_REACTION_ADDED) do(e:Args):
|
|
let args = MessageAddRemoveReactionArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onReactionAdded(args.messageId, args.emoji, args.reactionId)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_REACTION_REMOVED) do(e:Args):
|
|
let args = MessageAddRemoveReactionArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onReactionRemoved(args.messageId, args.emoji, args.reactionId)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_REACTION_FROM_OTHERS) do(e:Args):
|
|
let args = MessageAddRemoveReactionArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.toggleReactionFromOthers(args.messageId, args.emoji, args.reactionId, args.reactionFrom)
|
|
|
|
self.events.on(SIGNAL_REACTIONS_FOR_MESSAGE_LOADED) do(e:Args):
|
|
let args = ReactionsLoadedArgs(e)
|
|
if(self.chatId != args.chatId or args.messageId.len == 0):
|
|
return
|
|
self.delegate.onReactionsLoaded(args.messageId, args.reactions)
|
|
|
|
self.events.on(SIGNAL_CONTACT_NICKNAME_CHANGED) do(e: Args):
|
|
var args = ContactArgs(e)
|
|
self.delegate.onContactDetailsUpdated(args.contactId)
|
|
|
|
self.events.on(SIGNAL_CONTACT_UNTRUSTWORTHY) do(e: Args):
|
|
var args = TrustArgs(e)
|
|
self.delegate.onContactDetailsUpdated(args.publicKey)
|
|
|
|
self.events.on(SIGNAL_CONTACT_TRUSTED) do(e: Args):
|
|
var args = TrustArgs(e)
|
|
self.delegate.onContactDetailsUpdated(args.publicKey)
|
|
|
|
self.events.on(SIGNAL_REMOVED_TRUST_STATUS) do(e: Args):
|
|
var args = TrustArgs(e)
|
|
self.delegate.onContactDetailsUpdated(args.publicKey)
|
|
|
|
self.events.on(SIGNAL_CONTACT_UPDATED) do(e: Args):
|
|
var args = ContactArgs(e)
|
|
self.delegate.onContactDetailsUpdated(args.contactId)
|
|
if (args.contactId == self.chatId):
|
|
self.delegate.onMutualContactChanged()
|
|
|
|
let chatDto = self.getChatDetails()
|
|
if(chatDto.chatType == ChatType.OneToOne):
|
|
self.events.on(SIGNAL_CONTACT_ADDED) do(e: Args):
|
|
var args = ContactArgs(e)
|
|
if (args.contactId == self.chatId):
|
|
self.delegate.onMutualContactChanged()
|
|
|
|
self.events.on(SIGNAL_CONTACT_REMOVED) do(e: Args):
|
|
var args = ContactRemovedArgs(e)
|
|
if (args.contactId == self.chatId):
|
|
self.delegate.onMutualContactChanged()
|
|
|
|
self.events.on(SIGNAL_CONTACT_BLOCKED) do(e: Args):
|
|
var args = ContactArgs(e)
|
|
if (args.contactId == self.chatId):
|
|
self.delegate.onMutualContactChanged()
|
|
self.delegate.onContactDetailsUpdated(args.contactId)
|
|
|
|
self.events.on(SIGNAL_CONTACT_UNBLOCKED) do(e: Args):
|
|
var args = ContactArgs(e)
|
|
if (args.contactId == self.chatId):
|
|
self.delegate.onMutualContactChanged()
|
|
self.delegate.onContactDetailsUpdated(args.contactId)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_REMOVED) do(e: Args):
|
|
let args = MessageRemovedArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
# remove from pinned messages model
|
|
self.delegate.onUnpinMessage(args.messageId)
|
|
|
|
self.events.on(SIGNAL_MESSAGES_DELETED) do(e: Args):
|
|
let args = MessagesDeletedArgs(e)
|
|
if self.chatId in args.deletedMessages:
|
|
for deletedMessage in args.deletedMessages[self.chatId]:
|
|
# delete from pinned messages model
|
|
self.delegate.onUnpinMessage(deletedMessage)
|
|
|
|
self.events.on(SIGNAL_COMMUNITY_CHANNEL_EDITED) do(e:Args):
|
|
let args = CommunityChatArgs(e)
|
|
if(args.chat.communityId != self.sectionId or args.chat.id != self.chatId):
|
|
return
|
|
self.delegate.onCommunityChannelEdited(args.chat)
|
|
|
|
self.events.on(SIGNAL_CHAT_RENAMED) do(e: Args):
|
|
var args = ChatRenameArgs(e)
|
|
if(self.chatId != args.id):
|
|
return
|
|
self.delegate.onChatRenamed(args.newName)
|
|
|
|
self.events.on(SIGNAL_GROUP_CHAT_DETAILS_UPDATED) do(e: Args):
|
|
var args = ChatUpdateDetailsArgs(e)
|
|
if(self.chatId != args.id):
|
|
return
|
|
self.delegate.onGroupChatDetailsUpdated(args.newName, args.newColor, args.newImage)
|
|
|
|
self.events.on(SIGNAL_MESSAGE_EDITED) do(e: Args):
|
|
let args = MessageEditedArgs(e)
|
|
if(self.chatId != args.chatId):
|
|
return
|
|
self.delegate.onMessageEdited(args.message)
|
|
|
|
proc getMyChatId*(self: Controller): string =
|
|
return self.chatId
|
|
|
|
proc getChatDetails*(self: Controller): lent ChatDto =
|
|
return self.chatService.getChatById(self.chatId)
|
|
|
|
proc getCommunityDetails*(self: Controller): lent CommunityDto =
|
|
return self.communityService.getCommunityById(self.sectionId)
|
|
|
|
proc getOneToOneChatNameAndImage*(self: Controller): tuple[name: string, image: string, largeImage: string] =
|
|
return self.chatService.getOneToOneChatNameAndImage(self.chatId)
|
|
|
|
proc belongsToCommunity*(self: Controller): bool =
|
|
return self.belongsToCommunity
|
|
|
|
proc unpinMessage*(self: Controller, messageId: string) =
|
|
self.messageService.pinUnpinMessage(self.chatId, messageId, false)
|
|
|
|
proc getMessageById*(self: Controller, messageId: string): GetMessageResult =
|
|
return self.messageService.getMessageByMessageId(messageId)
|
|
|
|
proc isUsersListAvailable*(self: Controller): bool =
|
|
return self.isUsersListAvailable
|
|
|
|
proc muteChat*(self: Controller, interval: int) =
|
|
self.chatService.muteChat(self.chatId, interval)
|
|
|
|
proc unmuteChat*(self: Controller) =
|
|
self.chatService.unmuteChat(self.chatId)
|
|
|
|
proc unblockChat*(self: Controller) =
|
|
self.contactService.unblockContact(self.chatId)
|
|
|
|
proc markAllMessagesRead*(self: Controller) =
|
|
self.messageService.markAllMessagesRead(self.chatId)
|
|
|
|
proc markMessageRead*(self: Controller, msgID: string) =
|
|
self.messageService.markCertainMessagesRead(self.chatId, @[msgID])
|
|
|
|
proc clearChatHistory*(self: Controller) =
|
|
self.chatService.clearChatHistory(self.chatId)
|
|
|
|
proc leaveChat*(self: Controller) =
|
|
self.chatService.leaveChat(self.chatId)
|
|
|
|
proc asyncLoadReactionsForMessage*(self: Controller, messageId: string) =
|
|
self.messageService.asyncLoadReactionsForMessage(self.chatId, messageId)
|
|
|
|
proc getContactById*(self: Controller, contactId: string): ContactsDto =
|
|
return self.contactService.getContactById(contactId)
|
|
|
|
proc getContactDetails*(self: Controller, contactId: string): ContactDetails =
|
|
return self.contactService.getContactDetails(contactId)
|
|
|
|
proc getRenderedText*(self: Controller, parsedTextArray: seq[ParsedText], communityChats: openArray[ChatDto]): string =
|
|
return self.messageService.getRenderedText(parsedTextArray, communityChats)
|
|
|
|
proc getTransactionDetails*(self: Controller, message: MessageDto): (string,string) =
|
|
return self.messageService.getTransactionDetails(message)
|
|
|
|
proc getWalletAccounts*(self: Controller): seq[wallet_account_service.WalletAccountDto] =
|
|
return self.messageService.getWalletAccounts()
|