diff --git a/AGENTS.md b/AGENTS.md index 85c569ea5..2578c5110 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands. - **SwiftLint** (.swiftlint.yml) + **SwiftFormat** (.swiftformat) enforce style on build. Warnings show in Xcode. Build before **XcodeRefreshCodeIssuesInFile**. - Follow [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/) everywhere, Rust SDK wrappers too (`ID` not `Id`, `URL` not `Url`, `configuration` not `config` or `cfg`). - File headers live in `IDETemplateMacros.plist`. +- **Import `SwiftUI`, not `UIKit`** — need `UIPasteboard` etc, `SwiftUI` bring it. `SwiftUI` re-export `Foundation` too, so no `import Foundation` beside it. ### Comments @@ -52,6 +53,7 @@ PRs must follow rules. Prefer Xcode MCP tools over terminal commands. ## Pull Request Guidelines +- New branch from `develop` must not track `develop`. - Sentence-style titles (no conventional commits). - Exactly one `pr-` label (see `.github/release.yml`). - Title = changelog entry — descriptive, no "Fixes #…". diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift index 2b1412bb7..0519c857f 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenModels.swift @@ -21,6 +21,7 @@ struct BlockedUsersScreenViewStateBindings { enum BlockedUsersScreenViewAction { case unblockUser(UserProfile) + case copyUserID(UserProfile) } enum BlockedUsersScreenViewStateAlertType: Hashable { diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift index cdd66f6ea..5ff91d4b8 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/BlockedUsersScreenViewModel.swift @@ -51,6 +51,12 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers self?.unblockUser(user) }, secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil)) + case .copyUserID(let user): + UIPasteboard.general.string = user.id + userIndicatorController.submitIndicator(UserIndicator(id: Self.copiedIndicatorIdentifier, + type: .toast, + title: L10n.commonCopiedToClipboard, + icon: \.check)) } } @@ -108,6 +114,7 @@ class BlockedUsersScreenViewModel: BlockedUsersScreenViewModelType, BlockedUsers // MARK: Loading indicator private static let loadingIndicatorIdentifier = "\(BlockedUsersScreenViewModel.self)-Loading" + private static let copiedIndicatorIdentifier = "\(BlockedUsersScreenViewModel.self)-Copied" private func showLoadingIndicator() { userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier, diff --git a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift index a22ac0409..4113c16a0 100644 --- a/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift +++ b/ElementX/Sources/Screens/BlockedUsersScreen/View/BlockedUsersScreen.swift @@ -38,6 +38,13 @@ struct BlockedUsersScreen: View { icon: avatar(for: user)), details: .isWaiting(context.viewState.processingUserID == user.id), kind: .button { context.send(viewAction: .unblockUser(user)) }) + .contextMenu { + Button { + context.send(viewAction: .copyUserID(user)) + } label: { + Label(L10n.actionCopy, icon: \.copy) + } + } } } } diff --git a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift index dbe65cc05..bcf41cc20 100644 --- a/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift +++ b/UnitTests/Sources/BlockedUsersScreenViewModelTests.swift @@ -8,7 +8,7 @@ import Combine @testable import ElementX -import Foundation +import SwiftUI import Testing @MainActor @@ -42,4 +42,22 @@ struct BlockedUsersScreenViewModelTests { #expect(!viewModel.context.viewState.blockedUsers.isEmpty) #expect(clientProxy.profileForCalled) } + + @Test + func copyUserID() throws { + let clientProxy = ClientProxyMock(.init(userID: RoomMemberProxyMock.mockMe.userID)) + let userIndicatorController = UserIndicatorControllerMock() + + let viewModel = BlockedUsersScreenViewModel(hideProfiles: true, + userSession: UserSessionMock(.init(clientProxy: clientProxy)), + userIndicatorController: userIndicatorController) + + let user = try #require(viewModel.context.viewState.blockedUsers.first) + UIPasteboard.general.string = "" + + viewModel.context.send(viewAction: .copyUserID(user)) + + #expect(UIPasteboard.general.string == user.id) + #expect(userIndicatorController.submitIndicatorDelayReceivedArguments?.indicator.title == L10n.commonCopiedToClipboard) + } }