[codex] Refactor app runtime and ownership architecture (#1104)

* Refactor app runtime and view model architecture

* Move app ownership into stores and coordinators

* Fix smoke test environment injection

* Stabilize fragmentation package tests

* Fix coordinator build warnings

* Clean up chat view model warnings

* Fix Nostr relay startup coalescing

---------

Co-authored-by: jack <jackjackbits@users.noreply.github.com>
This commit is contained in:
jack
2026-05-30 14:13:26 +02:00
committed by GitHub
co-authored by jack
parent a6cb8872fb
commit 764f016d17
53 changed files with 10717 additions and 6192 deletions
+208 -24
View File
@@ -29,6 +29,151 @@ private func makeSmokeViewModel() -> (viewModel: ChatViewModel, transport: MockT
return (viewModel, transport, identityManager)
}
@MainActor
private struct SmokeFeatureModels {
let publicChatModel: PublicChatModel
let appChromeModel: AppChromeModel
let locationChannelsModel: LocationChannelsModel
let privateInboxModel: PrivateInboxModel
let privateConversationModel: PrivateConversationModel
let verificationModel: VerificationModel
let conversationUIModel: ConversationUIModel
let peerListModel: PeerListModel
}
@MainActor
private func makeSmokeLocationManager() -> LocationChannelManager {
let suiteName = "ViewSmokeTests.\(UUID().uuidString)"
let storage = UserDefaults(suiteName: suiteName) ?? .standard
storage.removePersistentDomain(forName: suiteName)
return LocationChannelManager(storage: storage)
}
@MainActor
private func makeSmokeFeatureModels(for viewModel: ChatViewModel) -> SmokeFeatureModels {
let locationManager = makeSmokeLocationManager()
let conversationStore = viewModel.conversationStore
let publicChatModel = PublicChatModel(conversationStore: conversationStore)
let locationChannelsModel = LocationChannelsModel(manager: locationManager)
let privateInboxModel = PrivateInboxModel(conversationStore: conversationStore)
let appChromeModel = AppChromeModel(
chatViewModel: viewModel,
privateInboxModel: privateInboxModel
)
let privateConversationModel = PrivateConversationModel(
chatViewModel: viewModel,
conversationStore: conversationStore,
locationChannelsModel: locationChannelsModel
)
let verificationModel = VerificationModel(
chatViewModel: viewModel,
privateConversationModel: privateConversationModel
)
let conversationUIModel = ConversationUIModel(
chatViewModel: viewModel,
privateConversationModel: privateConversationModel,
conversationStore: conversationStore
)
let peerListModel = PeerListModel(
chatViewModel: viewModel,
conversationStore: conversationStore,
locationChannelsModel: locationChannelsModel
)
return SmokeFeatureModels(
publicChatModel: publicChatModel,
appChromeModel: appChromeModel,
locationChannelsModel: locationChannelsModel,
privateInboxModel: privateInboxModel,
privateConversationModel: privateConversationModel,
verificationModel: verificationModel,
conversationUIModel: conversationUIModel,
peerListModel: peerListModel
)
}
@MainActor
private func installSmokeEnvironment<V: View>(
_ view: V,
featureModels: SmokeFeatureModels
) -> some View {
view
.environmentObject(featureModels.publicChatModel)
.environmentObject(featureModels.appChromeModel)
.environmentObject(featureModels.locationChannelsModel)
.environmentObject(featureModels.privateInboxModel)
.environmentObject(featureModels.privateConversationModel)
.environmentObject(featureModels.verificationModel)
.environmentObject(featureModels.conversationUIModel)
.environmentObject(featureModels.peerListModel)
}
@MainActor
private struct ContentPeopleSheetHarness: View {
@State private var showSidebar = true
@State private var messageText = ""
@State private var selectedMessageSender: String?
@State private var selectedMessageSenderID: PeerID?
@State private var imagePreviewURL: URL?
@State private var windowCountPublic = 300
@State private var windowCountPrivate: [PeerID: Int] = [:]
@State private var isAtBottomPrivate = true
@State private var autocompleteDebounceTimer: Timer?
@StateObject private var voiceRecordingVM = VoiceRecordingViewModel()
@FocusState private var isTextFieldFocused: Bool
#if os(iOS)
@State private var showImagePicker = false
@State private var imagePickerSourceType: UIImagePickerController.SourceType = .photoLibrary
#else
@State private var showMacImagePicker = false
#endif
var body: some View {
#if os(iOS)
ContentPeopleSheetView(
showSidebar: $showSidebar,
messageText: $messageText,
selectedMessageSender: $selectedMessageSender,
selectedMessageSenderID: $selectedMessageSenderID,
imagePreviewURL: $imagePreviewURL,
windowCountPublic: $windowCountPublic,
windowCountPrivate: $windowCountPrivate,
isAtBottomPrivate: $isAtBottomPrivate,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: .black,
textColor: .green,
secondaryTextColor: .gray,
headerHeight: 44,
onSendMessage: {},
showImagePicker: $showImagePicker,
imagePickerSourceType: $imagePickerSourceType
)
#else
ContentPeopleSheetView(
showSidebar: $showSidebar,
messageText: $messageText,
selectedMessageSender: $selectedMessageSender,
selectedMessageSenderID: $selectedMessageSenderID,
imagePreviewURL: $imagePreviewURL,
windowCountPublic: $windowCountPublic,
windowCountPrivate: $windowCountPrivate,
isAtBottomPrivate: $isAtBottomPrivate,
isTextFieldFocused: $isTextFieldFocused,
voiceRecordingVM: voiceRecordingVM,
autocompleteDebounceTimer: $autocompleteDebounceTimer,
backgroundColor: .black,
textColor: .green,
secondaryTextColor: .gray,
headerHeight: 44,
onSendMessage: {},
showMacImagePicker: $showMacImagePicker
)
#endif
}
}
@MainActor
@discardableResult
private func mount<V: View>(_ view: V) -> AnyObject {
@@ -112,11 +257,13 @@ private func makeTemporaryImageURL() throws -> URL {
return url
}
@Suite("View Smoke Tests", .serialized)
@MainActor
struct ViewSmokeTests {
@Test
func fingerprintView_renders_verifiedAndPendingStates() async {
let (viewModel, transport, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let verifiedPeer = PeerID(str: "0102030405060708")
let pendingPeer = PeerID(str: "1112131415161718")
let verifiedFingerprint = String(repeating: "ab", count: 32)
@@ -131,11 +278,11 @@ struct ViewSmokeTests {
viewModel.verifiedFingerprints.insert(verifiedFingerprint)
let verifiedView = FingerprintView(viewModel: viewModel, peerID: verifiedPeer)
let pendingView = FingerprintView(viewModel: viewModel, peerID: pendingPeer)
let verifiedView = FingerprintView(peerID: verifiedPeer)
.environmentObject(featureModels.verificationModel)
let pendingView = FingerprintView(peerID: pendingPeer)
.environmentObject(featureModels.verificationModel)
_ = verifiedView.body
_ = pendingView.body
_ = mount(verifiedView)
_ = mount(pendingView)
@@ -145,13 +292,14 @@ struct ViewSmokeTests {
@Test
func verificationViews_renderCoreBranches() throws {
let (viewModel, transport, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let peerID = PeerID(str: "2122232425262728")
let fingerprint = String(repeating: "cd", count: 32)
var isPresented = true
transport.peerFingerprints[peerID] = fingerprint
transport.updatePeerSnapshots([makeSnapshot(peerID: peerID, nickname: "Verifier", noiseByte: 0x33)])
viewModel.selectedPrivateChatPeer = peerID
featureModels.privateConversationModel.startConversation(with: peerID)
viewModel.verifiedFingerprints.insert(fingerprint)
let image = try makeCGImage()
@@ -173,35 +321,36 @@ struct ViewSmokeTests {
set: { isPresented = $0 }
)
)
.environmentObject(viewModel)
.environmentObject(featureModels.verificationModel)
)
}
@Test
func meshPeerList_renders_emptyAndPopulatedStates() async {
let (viewModel, transport, identityManager) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let connectedPeer = PeerID(str: "3132333435363738")
let blockedPeer = PeerID(str: "4142434445464748")
let blockedFingerprint = String(repeating: "ef", count: 32)
_ = mount(
MeshPeerList(
viewModel: viewModel,
textColor: .green,
secondaryTextColor: .gray,
onTapPeer: { _ in },
onToggleFavorite: { _ in },
onShowFingerprint: { _ in }
)
.environmentObject(featureModels.peerListModel)
)
_ = MeshPeerList(
viewModel: viewModel,
textColor: .green,
secondaryTextColor: .gray,
onTapPeer: { _ in },
onToggleFavorite: { _ in },
onShowFingerprint: { _ in }
).body
)
.environmentObject(featureModels.peerListModel)
transport.peerFingerprints[blockedPeer] = blockedFingerprint
identityManager.setBlocked(blockedFingerprint, isBlocked: true)
@@ -214,13 +363,13 @@ struct ViewSmokeTests {
_ = mount(
MeshPeerList(
viewModel: viewModel,
textColor: .green,
secondaryTextColor: .gray,
onTapPeer: { _ in },
onToggleFavorite: { _ in },
onShowFingerprint: { _ in }
)
.environmentObject(featureModels.peerListModel)
)
#expect(viewModel.hasUnreadMessages(for: blockedPeer))
@@ -229,10 +378,11 @@ struct ViewSmokeTests {
@Test
func commandSuggestionsAndLocationViews_render() {
let (viewModel, _, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let channel = GeohashChannel(level: .city, geohash: "u4pruy")
var messageText = "/f"
LocationChannelManager.shared.select(.location(channel))
featureModels.locationChannelsModel.select(.location(channel))
_ = mount(
CommandSuggestionsView(
@@ -244,22 +394,25 @@ struct ViewSmokeTests {
backgroundColor: .black,
secondaryTextColor: .gray
)
.environmentObject(viewModel)
.environmentObject(featureModels.privateConversationModel)
.environmentObject(featureModels.locationChannelsModel)
)
_ = mount(
LocationChannelsSheet(isPresented: .constant(true))
.environmentObject(viewModel)
.environmentObject(featureModels.locationChannelsModel)
.environmentObject(featureModels.peerListModel)
)
#expect(messageText == "/f")
LocationChannelManager.shared.select(.mesh)
LocationChannelManager.shared.endLiveRefresh()
featureModels.locationChannelsModel.select(.mesh)
featureModels.locationChannelsModel.endLiveRefresh()
}
@Test
func locationNotesView_rendersNoRelayAndLoadedStates() throws {
let (viewModel, _, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let noRelayManager = LocationNotesManager(
geohash: "u4pruydq",
@@ -302,12 +455,20 @@ struct ViewSmokeTests {
eose?()
_ = mount(
LocationNotesView(geohash: "u4pruydq", manager: noRelayManager)
.environmentObject(viewModel)
LocationNotesView(
geohash: "u4pruydq",
senderNickname: viewModel.nickname,
manager: noRelayManager
)
.environmentObject(featureModels.locationChannelsModel)
)
_ = mount(
LocationNotesView(geohash: "u4pruydq", manager: loadedManager)
.environmentObject(viewModel)
LocationNotesView(
geohash: "u4pruydq",
senderNickname: viewModel.nickname,
manager: loadedManager
)
.environmentObject(featureModels.locationChannelsModel)
)
#expect(loadedManager.notes.count == 1)
@@ -350,11 +511,34 @@ struct ViewSmokeTests {
#expect(PaymentChipView.PaymentType.lightning("lightning:lnbc1test").url?.scheme == "lightning")
}
@Test
func contentShellViews_renderPublicAndPrivateBranches() async {
let (viewModel, transport, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let peerID = PeerID(str: "5152535455565758")
transport.updatePeerSnapshots([
makeSnapshot(peerID: peerID, nickname: "Alice", noiseByte: 0x66)
])
try? await Task.sleep(nanoseconds: 50_000_000)
_ = mount(installSmokeEnvironment(ContentView(), featureModels: featureModels))
_ = mount(installSmokeEnvironment(ContentPeopleSheetHarness(), featureModels: featureModels))
featureModels.privateConversationModel.startConversation(with: peerID)
try? await Task.sleep(nanoseconds: 50_000_000)
_ = mount(installSmokeEnvironment(ContentPeopleSheetHarness(), featureModels: featureModels))
#expect(featureModels.privateConversationModel.selectedPeerID == peerID)
#expect(featureModels.privateConversationModel.selectedHeaderState?.headerPeerID == peerID)
}
@Test
func geohashAndTextMessageViews_renderCoreBranches() {
let (viewModel, _, _) = makeSmokeViewModel()
let featureModels = makeSmokeFeatureModels(for: viewModel)
let geohashPeopleList = GeohashPeopleList(
viewModel: viewModel,
textColor: .green,
secondaryTextColor: .gray,
onTapPerson: {}
@@ -377,10 +561,10 @@ struct ViewSmokeTests {
deliveryStatus: .partiallyDelivered(reached: 1, total: 2)
)
_ = geohashPeopleList.body
_ = mount(geohashPeopleList)
_ = mount(TextMessageView(message: truncatableMessage).environmentObject(viewModel))
_ = mount(TextMessageView(message: paymentMessage).environmentObject(viewModel))
_ = mount(geohashPeopleList.environmentObject(featureModels.peerListModel))
_ = mount(geohashPeopleList.environmentObject(featureModels.peerListModel))
_ = mount(TextMessageView(message: truncatableMessage).environmentObject(featureModels.conversationUIModel))
_ = mount(TextMessageView(message: paymentMessage).environmentObject(featureModels.conversationUIModel))
#expect(truncatableMessage.content.count > TransportConfig.uiLongMessageLengthThreshold)
#expect(paymentMessage.content.contains("lightning:") && paymentMessage.content.contains("cashu"))