Fix some concurrency warnings, update missed licence headers. (#3741)

* Switch the TimelineController to an async sequence and fix the warnings on the UserIndicatorController
This commit is contained in:
Stefan Ceriu 2025-02-06 11:35:23 +02:00 committed by GitHub
parent f86a5a2bb9
commit 922ebf47e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 129 additions and 184 deletions

View File

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
command -v git-lfs >/dev/null 2>&1 || { echo >&2 "\nThis repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks').\n"; exit 2; } command -v git-lfs >/dev/null 2>&1 || { printf >&2 "\n%s\n\n" "This repository is configured for Git LFS but 'git-lfs' was not found on your path. If you no longer wish to use Git LFS, remove this hook by deleting the 'pre-push' file in the hooks directory (set by 'core.hookspath'; usually '.git/hooks')."; exit 2; }
git lfs pre-push "$@" git lfs pre-push "$@"

View File

@ -7,6 +7,7 @@
import Foundation import Foundation
@MainActor
protocol AppCoordinatorProtocol: CoordinatorProtocol { protocol AppCoordinatorProtocol: CoordinatorProtocol {
var windowManager: SecureWindowManagerProtocol { get } var windowManager: SecureWindowManagerProtocol { get }

View File

@ -1,17 +1,8 @@
// //
// Copyright 2024 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Combine import Combine

View File

@ -8,6 +8,7 @@
import Foundation import Foundation
/// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding). /// Represents a specific portion of the ViewState that can be bound to with SwiftUI's [2-way binding](https://developer.apple.com/documentation/swiftui/binding).
@MainActor
protocol BindableState { protocol BindableState {
/// The associated type of the Bindable State. Defaults to Void. /// The associated type of the Bindable State. Defaults to Void.
associatedtype BindStateType = Void associatedtype BindStateType = Void

View File

@ -5,10 +5,11 @@
// Please see LICENSE files in the repository root for full details. // Please see LICENSE files in the repository root for full details.
// //
import Combine
import SwiftUI import SwiftUI
class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol { class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol {
private var dismissalTimer: Timer? private var timerCancellable: AnyCancellable?
private var displayTimes = [String: Date]() private var displayTimes = [String: Date]()
private var delayedIndicators = Set<String>() private var delayedIndicators = Set<String>()
@ -21,10 +22,11 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
activeIndicator = indicatorQueue.last activeIndicator = indicatorQueue.last
if let activeIndicator, !activeIndicator.persistent { if let activeIndicator, !activeIndicator.persistent {
dismissalTimer?.invalidate() timerCancellable?.cancel()
dismissalTimer = Timer.scheduledTimer(withTimeInterval: nonPersistentDisplayDuration, repeats: false) { [weak self] _ in timerCancellable = Task { [weak self, nonPersistentDisplayDuration] in
try await Task.sleep(for: .seconds(nonPersistentDisplayDuration))
self?.retractIndicatorWithId(activeIndicator.id) self?.retractIndicatorWithId(activeIndicator.id)
} }.asCancellable()
} }
} }
} }
@ -47,8 +49,8 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
if let delay { if let delay {
delayedIndicators.insert(indicator.id) delayedIndicators.insert(indicator.id)
Timer.scheduledTimer(withTimeInterval: delay.seconds, repeats: false) { [weak self] _ in Task {
guard let self else { return } try await Task.sleep(for: .seconds(delay.seconds))
guard delayedIndicators.contains(indicator.id) else { guard delayedIndicators.contains(indicator.id) else {
return return
@ -76,9 +78,10 @@ class UserIndicatorController: ObservableObject, UserIndicatorControllerProtocol
return return
} }
Timer.scheduledTimer(withTimeInterval: minimumDisplayDuration, repeats: false) { [weak self] _ in Task {
self?.indicatorQueue.removeAll { $0.id == id } try? await Task.sleep(for: .seconds(minimumDisplayDuration))
self?.displayTimes[id] = nil indicatorQueue.removeAll { $0.id == id }
displayTimes[id] = nil
} }
} }

View File

@ -7,6 +7,7 @@
import Combine import Combine
@MainActor
protocol SoftLogoutScreenViewModelProtocol { protocol SoftLogoutScreenViewModelProtocol {
var actions: AnyPublisher<SoftLogoutScreenViewModelAction, Never> { get } var actions: AnyPublisher<SoftLogoutScreenViewModelAction, Never> { get }
var context: SoftLogoutScreenViewModelType.Context { get } var context: SoftLogoutScreenViewModelType.Context { get }

View File

@ -81,7 +81,7 @@ struct WebRegistrationWebView: UIViewRepresentable {
webView.load(URLRequest(url: url)) webView.load(URLRequest(url: url))
} }
nonisolated func userContentController(_ userContentController: WKUserContentController, func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) { didReceive message: WKScriptMessage) {
guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else { guard let jsonString = message.body as? String, let jsonData = jsonString.data(using: .utf8) else {
MXLog.error("Unexpected response.") MXLog.error("Unexpected response.")
@ -94,7 +94,7 @@ struct WebRegistrationWebView: UIViewRepresentable {
} }
MXLog.info("Received login credentials.") MXLog.info("Received login credentials.")
Task { await viewModelContext.send(viewAction: .signedIn(credentials)) } viewModelContext.send(viewAction: .signedIn(credentials))
} }
// MARK: WKUIDelegate // MARK: WKUIDelegate
@ -120,7 +120,7 @@ struct WebRegistrationWebView: UIViewRepresentable {
// MARK: WKScriptMessageHandler // MARK: WKScriptMessageHandler
nonisolated func userContentController(_ userContentController: WKUserContentController, func userContentController(_ userContentController: WKUserContentController,
didReceive message: WKScriptMessage) { didReceive message: WKScriptMessage) {
coordinator?.userContentController(userContentController, didReceive: message) coordinator?.userContentController(userContentController, didReceive: message)
} }

View File

@ -152,11 +152,8 @@ private struct CallView: UIViewRepresentable {
} }
} }
nonisolated func userContentController(_ userContentController: WKUserContentController, func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
didReceive message: WKScriptMessage) { viewModelContext?.javaScriptMessageHandler?(message.body)
Task { @MainActor [weak self] in
self?.viewModelContext?.javaScriptMessageHandler?(message.body)
}
} }
// MARK: - WKUIDelegate // MARK: - WKUIDelegate
@ -191,11 +188,9 @@ private struct CallView: UIViewRepresentable {
return .cancel return .cancel
} }
nonisolated func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
Task { @MainActor in
viewModelContext?.send(viewAction: .urlChanged(webView.url)) viewModelContext?.send(viewAction: .urlChanged(webView.url))
} }
}
// MARK: - Picture in Picture // MARK: - Picture in Picture
@ -271,8 +266,7 @@ private struct CallView: UIViewRepresentable {
// MARK: - WKScriptMessageHandler // MARK: - WKScriptMessageHandler
nonisolated func userContentController(_ userContentController: WKUserContentController, func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
didReceive message: WKScriptMessage) {
coordinator?.userContentController(userContentController, didReceive: message) coordinator?.userContentController(userContentController, didReceive: message)
} }
} }

View File

@ -6,7 +6,6 @@
// //
import Combine import Combine
import Foundation
@MainActor @MainActor
protocol ReportContentScreenViewModelProtocol { protocol ReportContentScreenViewModelProtocol {

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Combine import Combine

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Foundation import Foundation

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Combine import Combine

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Combine import Combine

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import Compound import Compound

View File

@ -9,6 +9,7 @@ import Combine
import WysiwygComposer import WysiwygComposer
// periphery: ignore - markdown protocol // periphery: ignore - markdown protocol
@MainActor
protocol ComposerToolbarViewModelProtocol { protocol ComposerToolbarViewModelProtocol {
var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get } var actions: AnyPublisher<ComposerToolbarViewModelAction, Never> { get }
var context: ComposerToolbarViewModelType.Context { get } var context: ComposerToolbarViewModelType.Context { get }

View File

@ -6,8 +6,8 @@
// //
import Combine import Combine
import Foundation
@MainActor
protocol RoomScreenViewModelProtocol { protocol RoomScreenViewModelProtocol {
var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get } var actions: AnyPublisher<RoomScreenViewModelAction, Never> { get }
var context: RoomScreenViewModel.Context { get } var context: RoomScreenViewModel.Context { get }

View File

@ -324,16 +324,12 @@ class ClientProxy: ClientProxyProtocol {
// Note: This isn't strictly necessary now given the unwrap above, but leaving the code as // Note: This isn't strictly necessary now given the unwrap above, but leaving the code as
// documentation. SE-0371 will allow us to fix this by using an async deinit. // documentation. SE-0371 will allow us to fix this by using an async deinit.
Task { [syncService] in Task { [syncService] in
do {
defer { defer {
completion?() completion?()
} }
try await syncService.stop() await syncService.stop()
MXLog.info("Sync stopped") MXLog.info("Sync stopped")
} catch {
MXLog.error("Failed stopping the sync service with error: \(error)")
}
} }
} }

View File

@ -46,7 +46,7 @@ final class ComposerDraftService: ComposerDraftServiceProtocol {
func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> { func getReply(eventID: String) async -> Result<TimelineItemReply, ComposerDraftServiceError> {
switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) { switch await roomProxy.timeline.getLoadedReplyDetails(eventID: eventID) {
case .success(let replyDetails): case .success(let replyDetails):
return await .success(timelineItemfactory.buildReply(details: replyDetails)) return .success(timelineItemfactory.buildReply(details: replyDetails))
case .failure(let error): case .failure(let error):
MXLog.error("Could not load reply: \(error)") MXLog.error("Could not load reply: \(error)")
return .failure(.failedToLoadReply) return .failure(.failedToLoadReply)

View File

@ -62,8 +62,6 @@ class TimelineController: TimelineControllerProtocol {
activeTimeline = timelineProxy activeTimeline = timelineProxy
activeTimelineProvider = liveTimelineProvider activeTimelineProvider = liveTimelineProvider
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
guard let initialFocussedEventID else { guard let initialFocussedEventID else {
configureActiveTimelineProvider() configureActiveTimelineProvider()
return return
@ -148,7 +146,9 @@ class TimelineController: TimelineControllerProtocol {
} }
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol { if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
fetchEventDetails(for: messageTimelineItem, refetchOnError: true) fetchEventDetails(for: messageTimelineItem,
refetchOnError: true,
activeTimeline: activeTimeline)
} }
} }
@ -375,35 +375,37 @@ class TimelineController: TimelineControllerProtocol {
paginationState = PaginationState(backward: .paginating, forward: .paginating) paginationState = PaginationState(backward: .paginating, forward: .paginating)
callbacks.send(.isLive(activeTimelineProvider.kind == .live)) callbacks.send(.isLive(activeTimelineProvider.kind == .live))
updateTimelineItemsCancellable = activeTimelineProvider updateTimelineItemsCancellable = Task { [weak self, activeTimelineProvider] in
.updatePublisher let contentSizeChangePublisher = NotificationCenter.default.publisher(for: UIContentSizeCategory.didChangeNotification)
.receive(on: serialDispatchQueue) let timelineUpdates = activeTimelineProvider.updatePublisher.merge(with: contentSizeChangePublisher.map { _ in
.sink { [weak self] items, paginationState in (activeTimelineProvider.itemProxies, activeTimelineProvider.paginationState)
self?.updateTimelineItems(itemProxies: items, paginationState: paginationState) })
for await (items, paginationState) in timelineUpdates.values {
await self?.updateTimelineItems(itemProxies: items, paginationState: paginationState)
} }
}.asCancellable()
} }
@objc private func contentSizeCategoryDidChange() { private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) async {
// Recompute all attributed strings on content size changes -> DynamicType support
serialDispatchQueue.async { [activeTimelineProvider] in
self.updateTimelineItems(itemProxies: activeTimelineProvider.itemProxies, paginationState: activeTimelineProvider.paginationState)
}
}
private func updateTimelineItems(itemProxies: [TimelineItemProxy], paginationState: PaginationState) {
var newTimelineItems = [RoomTimelineItemProtocol]()
let isNewTimeline = isSwitchingTimelines let isNewTimeline = isSwitchingTimelines
isSwitchingTimelines = false isSwitchingTimelines = false
let collapsibleChunks = itemProxies.groupBy { isItemCollapsible($0) } let isDM = roomProxy.isDirectOneToOneRoom
var newTimelineItems = await Task.detached { [timelineItemFactory, activeTimeline] in
var newTimelineItems = [RoomTimelineItemProtocol]()
let collapsibleChunks = itemProxies.groupBy { $0.isItemCollapsible }
for (index, collapsibleChunk) in collapsibleChunks.enumerated() { for (index, collapsibleChunk) in collapsibleChunks.enumerated() {
let isLastItem = index == collapsibleChunks.indices.last let isLastItem = index == collapsibleChunks.indices.last
let items = collapsibleChunk.compactMap { itemProxy in let items = collapsibleChunk.compactMap { itemProxy in
let timelineItem = self.buildTimelineItem(for: itemProxy,
let timelineItem = buildTimelineItem(for: itemProxy) isDM: isDM,
timelineItemFactory: timelineItemFactory,
activeTimeline: activeTimeline)
return timelineItem return timelineItem
} }
@ -425,6 +427,9 @@ class TimelineController: TimelineControllerProtocol {
} }
} }
return newTimelineItems
}.value
// Check if we need to add anything to the top of the timeline. // Check if we need to add anything to the top of the timeline.
switch paginationState.backward { switch paginationState.backward {
case .timelineEndReached: case .timelineEndReached:
@ -445,23 +450,26 @@ class TimelineController: TimelineControllerProtocol {
break break
} }
DispatchQueue.main.sync {
timelineItems = newTimelineItems timelineItems = newTimelineItems
}
callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline)) callbacks.send(.updatedTimelineItems(timelineItems: newTimelineItems, isSwitchingTimelines: isNewTimeline))
self.paginationState = paginationState self.paginationState = paginationState
} }
private func buildTimelineItem(for itemProxy: TimelineItemProxy) -> RoomTimelineItemProtocol? { private nonisolated func buildTimelineItem(for itemProxy: TimelineItemProxy,
isDM: Bool,
timelineItemFactory: RoomTimelineItemFactoryProtocol,
activeTimeline: TimelineProxyProtocol) -> RoomTimelineItemProtocol? {
switch itemProxy { switch itemProxy {
case .event(let eventTimelineItem): case .event(let eventTimelineItem):
let timelineItem = timelineItemFactory.buildTimelineItem(for: eventTimelineItem, isDM: roomProxy.isDirectOneToOneRoom) let timelineItem = timelineItemFactory.buildTimelineItem(for: eventTimelineItem, isDM: isDM)
if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol { if let messageTimelineItem = timelineItem as? EventBasedMessageTimelineItemProtocol {
// Avoid fetching this over and over again as it changes states if it keeps failing to load // Avoid fetching this over and over again as it changes states if it keeps failing to load
// Errors will be handled again on appearance // Errors will be handled again on appearance
fetchEventDetails(for: messageTimelineItem, refetchOnError: false) fetchEventDetails(for: messageTimelineItem,
refetchOnError: false,
activeTimeline: activeTimeline)
} }
return timelineItem return timelineItem
@ -478,20 +486,9 @@ class TimelineController: TimelineControllerProtocol {
} }
} }
private func isItemCollapsible(_ item: TimelineItemProxy) -> Bool { private nonisolated func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol,
if case let .event(eventItem) = item { refetchOnError: Bool,
switch eventItem.content { activeTimeline: TimelineProxyProtocol) {
case .profileChange, .roomMembership, .state:
return true
default:
return false
}
}
return false
}
private func fetchEventDetails(for timelineItem: EventBasedMessageTimelineItemProtocol, refetchOnError: Bool) {
guard let eventID = timelineItem.id.eventID else { guard let eventID = timelineItem.id.eventID else {
return return
} }
@ -524,3 +521,18 @@ class TimelineController: TimelineControllerProtocol {
return nil return nil
} }
} }
private extension TimelineItemProxy {
var isItemCollapsible: Bool {
if case let .event(eventItem) = self {
switch eventItem.content {
case .profileChange, .roomMembership, .state:
return true
default:
return false
}
}
return false
}
}

View File

@ -9,7 +9,6 @@ import Foundation
import MatrixRustSDK import MatrixRustSDK
@MainActor
protocol RoomTimelineItemFactoryProtocol { protocol RoomTimelineItemFactoryProtocol {
func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol? func buildTimelineItem(for eventItemProxy: EventTimelineItemProxy, isDM: Bool) -> RoomTimelineItemProtocol?
func buildReply(details: InReplyToDetails) -> TimelineItemReply func buildReply(details: InReplyToDetails) -> TimelineItemReply

View File

@ -1,17 +1,8 @@
// //
// Copyright 2022 New Vector Ltd // Copyright 2022-2024 New Vector Ltd.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// you may not use this file except in compliance with the License. // Please see LICENSE files in the repository root for full details.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// //
import XCTest import XCTest

View File

@ -9,6 +9,7 @@ import XCTest
@testable import ElementX @testable import ElementX
@MainActor
class ServerConfirmationScreenViewStateTests: XCTestCase { class ServerConfirmationScreenViewStateTests: XCTestCase {
func testLoginMessageString() { func testLoginMessageString() {
let matrixDotOrgLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address, let matrixDotOrgLogin = ServerConfirmationScreenViewState(homeserverAddress: LoginHomeserver.mockMatrixDotOrg.address,