diff --git a/WireMessaging/Sources/WireMessagingDomain/ConversationCreation/ConversationCreationRepositoryProtocol.swift b/WireMessaging/Sources/WireMessagingDomain/ConversationCreation/ConversationCreationRepositoryProtocol.swift index 6a0e3682e95..df71ee7a9e8 100644 --- a/WireMessaging/Sources/WireMessagingDomain/ConversationCreation/ConversationCreationRepositoryProtocol.swift +++ b/WireMessaging/Sources/WireMessagingDomain/ConversationCreation/ConversationCreationRepositoryProtocol.swift @@ -17,4 +17,14 @@ // // sourcery: AutoMockable -public protocol ConversationCreationRepositoryProtocol {} +public protocol ConversationCreationRepositoryProtocol { + + /// Legacy services (bots) are deprecated and cannot be set up any more. However, teams who have bots already set up + /// may continue using them. + /// While the `apps` feature flag controls if the UI allows for starting a conversation with an app (new-style + /// MLS-only service) or adding an app to a conversation, there is no feature flag for bots (old-style Proteus-only + /// services). If there are bots already added to the team, bots are considered enabled. + + func areBotsSetUpInTheTeam() async throws -> Bool + +} diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/FeatureConfig/AppsFeatureConfigDecoder.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/FeatureConfig/AppsFeatureConfigDecoder.swift new file mode 100644 index 00000000000..944ce44a096 --- /dev/null +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/FeatureConfig/AppsFeatureConfigDecoder.swift @@ -0,0 +1,34 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation + +struct AppsFeatureConfigDecoder { + + func decode( + from container: KeyedDecodingContainer + ) throws -> AppsFeatureConfig { + let payload = try container.decode( + FeatureWithoutConfig.self, + forKey: .payload + ) + + return AppsFeatureConfig(status: payload.status.toAPIModel()) + } + +} diff --git a/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/UpdateEventDecodingProxy+FeatureConfig.swift b/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/UpdateEventDecodingProxy+FeatureConfig.swift index fb2b4b8092d..35ebdb246c2 100644 --- a/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/UpdateEventDecodingProxy+FeatureConfig.swift +++ b/WireNetwork/Sources/WireNetwork/APIs/Rest/UpdateEventsAPI/Event decoding/UpdateEventDecodingProxy+FeatureConfig.swift @@ -40,6 +40,11 @@ extension UpdateEventDecodingProxy { let event = FeatureConfigUpdateEvent(featureConfig: .appLock(config)) updateEvent = .featureConfig(.update(event)) + case "apps": + let config = try AppsFeatureConfigDecoder().decode(from: container) + let event = FeatureConfigUpdateEvent(featureConfig: .apps(config)) + updateEvent = .featureConfig(.update(event)) + case "classifiedDomains": let config = try ClassifiedDomainsFeatureConfigDecoder().decode(from: container) let event = FeatureConfigUpdateEvent(featureConfig: .classifiedDomains(config)) diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift index 1c1b1be1c2b..defb66342b5 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchRequest.swift @@ -49,6 +49,7 @@ public struct SearchOptions: OptionSet { /// Services which are enabled in your team. public static let services = SearchOptions(rawValue: 1 << 6) + // TODO: [WPB-20362] consider renaming to `bots` and adding `apps` /// Users from federated servers. diff --git a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift index 9a9227d9c92..aa5310ab969 100644 --- a/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift +++ b/wire-ios-sync-engine/Source/UserSession/Search/SearchResult.swift @@ -42,7 +42,7 @@ public struct SearchResult { /// Bots. - public var services: [ServiceUser] + public var services: [ServiceUser] // TODO: [WPB-20362] add `apps` /// Cache for search users. diff --git a/wire-ios-utilities/Source/DeveloperFlag.swift b/wire-ios-utilities/Source/DeveloperFlag.swift index 25302e08dc5..fe09b593497 100644 --- a/wire-ios-utilities/Source/DeveloperFlag.swift +++ b/wire-ios-utilities/Source/DeveloperFlag.swift @@ -24,7 +24,6 @@ public enum DeveloperFlag: String, CaseIterable { case channelsHistory case chatBubbles - case considerAppsFeatureFlag case consumableNotifications case createLegacyBackups case debugDuplicateObjects @@ -45,11 +44,6 @@ public enum DeveloperFlag: String, CaseIterable { public var description: String { switch self { - case .considerAppsFeatureFlag: - "Apps are not fully supported by the backend yet (e.g. no search endpoint available yet). However, some " + - "customers already have the apps feature flag enabled as a workaround for another issue." + - "If this toggle is off, the apps feature flag is ignored. Toggle it on for development." - case .createLegacyBackups: "Don't use the cross-platform library when creating backups." diff --git a/wire-ios/Wire-iOS Tests/AddParticipantsViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/AddParticipantsViewControllerSnapshotTests.swift index 482023af0fc..6b448e13730 100644 --- a/wire-ios/Wire-iOS Tests/AddParticipantsViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/AddParticipantsViewControllerSnapshotTests.swift @@ -80,6 +80,7 @@ final class AddParticipantsViewControllerSnapshotTests: XCTestCase { let newValues = ConversationCreationValues( isChannel: false, isAppsFeatureEnabled: true, + areLegacyBotsAvailable: false, name: "", participants: [], allowGuests: true, diff --git a/wire-ios/Wire-iOS Tests/ConversationCreationControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/ConversationCreationControllerSnapshotTests.swift index 01e579b051a..13f51d443c8 100644 --- a/wire-ios/Wire-iOS Tests/ConversationCreationControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationCreationControllerSnapshotTests.swift @@ -86,7 +86,10 @@ final class ConversationCreationControllerSnapshotTests: XCTestCase { // MARK: - Helper Method - private func createSut(isTeamMember: Bool, messageProtocol: Feature.MLS.Config.MessageProtocol = .proteus) async { + private func createSut( + isTeamMember: Bool, + messageProtocol: Feature.MLS.Config.MessageProtocol = .proteus + ) async { let mockSelfUser = MockUserType.createSelfUser(name: "Alice", inTeam: isTeamMember ? UUID() : nil) let mockUserSession = UserSessionMock(mockUser: mockSelfUser) mockUserSession.isWireCellsEnabled = true @@ -98,9 +101,11 @@ final class ConversationCreationControllerSnapshotTests: XCTestCase { ) ) - sut = await ConversationCreationController( + sut = ConversationCreationController( preSelectedParticipants: nil, - userSession: mockUserSession + userSession: mockUserSession, + isAppsFeatureEnabled: false, + areLegacyBotsAvailable: false ) } } diff --git a/wire-ios/Wire-iOS Tests/ConversationDetail/ConversationDetailsTests.swift b/wire-ios/Wire-iOS Tests/ConversationDetail/ConversationDetailsTests.swift index b1a721fc56a..0063a2b7efc 100644 --- a/wire-ios/Wire-iOS Tests/ConversationDetail/ConversationDetailsTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationDetail/ConversationDetailsTests.swift @@ -19,6 +19,7 @@ import Foundation import WireTestingPackage import XCTest + @testable import Wire final class ConversationDetailsTests: XCTestCase { @@ -45,13 +46,21 @@ final class ConversationDetailsTests: XCTestCase { conversation.isChannel = false XCTAssertFalse( - sut.accessible(in: conversation, by: user) + sut.accessible( + in: conversation, + by: user, + areLegacyBotsAvailable: false + ) ) } func testAccessOptionNotAllowed_ForChannel_Member() { XCTAssertFalse( - sut.accessible(in: conversation, by: user) + sut.accessible( + in: conversation, + by: user, + areLegacyBotsAvailable: false + ) ) } diff --git a/wire-ios/Wire-iOS Tests/ConversationDetail/GroupDetailsViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/ConversationDetail/GroupDetailsViewControllerSnapshotTests.swift index a3e75d8a89b..1e572ddabc7 100644 --- a/wire-ios/Wire-iOS Tests/ConversationDetail/GroupDetailsViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationDetail/GroupDetailsViewControllerSnapshotTests.swift @@ -108,7 +108,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -130,7 +132,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -163,7 +167,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -185,7 +191,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -206,7 +214,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -229,7 +239,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) snapshotHelper.verify(matching: sut) @@ -250,7 +262,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -272,7 +286,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -294,7 +310,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -316,7 +334,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) // THEN @@ -340,7 +360,9 @@ final class GroupDetailsViewControllerSnapshotTests: XCTestCase { mainCoordinator: mockMainCoordinator, selfProfileUIBuilder: MockSelfProfileViewControllerBuilderProtocol(), conversationCreationRepository: MockConversationCreationRepositoryProtocol(), - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: false, + isAppsFeatureEnabled: false ) snapshotHelper.verify(matching: sut) diff --git a/wire-ios/Wire-iOS Tests/ConversationList/Container/ConversationListViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/ConversationList/Container/ConversationListViewControllerSnapshotTests.swift index 739bbad0ee4..f3314eacd78 100644 --- a/wire-ios/Wire-iOS Tests/ConversationList/Container/ConversationListViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationList/Container/ConversationListViewControllerSnapshotTests.swift @@ -73,6 +73,7 @@ final class ConversationListViewControllerSnapshotTests: XCTestCase { zClientViewController = ZClientViewController( account: coreDataStack.account, + contextProvider: DefaultManagedObjectContextProvider(contextProvider: coreDataStack), selfProfileViewsMonitor: SelfProfileViewsMonitorImplementation(), userSession: userSession, trackingManager: nil, diff --git a/wire-ios/Wire-iOS Tests/ConversationOptionsServicesViewControllerTests.swift b/wire-ios/Wire-iOS Tests/ConversationServicesOptionsViewControllerTests.swift similarity index 97% rename from wire-ios/Wire-iOS Tests/ConversationOptionsServicesViewControllerTests.swift rename to wire-ios/Wire-iOS Tests/ConversationServicesOptionsViewControllerTests.swift index 644f22aab36..6b64710ab37 100644 --- a/wire-ios/Wire-iOS Tests/ConversationOptionsServicesViewControllerTests.swift +++ b/wire-ios/Wire-iOS Tests/ConversationServicesOptionsViewControllerTests.swift @@ -16,6 +16,7 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireDataModel import WireTestingPackage import XCTest @@ -25,6 +26,10 @@ final class MockServicesOptionsViewModelConfiguration: ConversationServicesOptio // MARK: Properties typealias SetHandler = (Bool, (Result) -> Void) -> Void + + var messageProtocol: MessageProtocol = .proteus + var areLegacyBotsAvailable = false + var isAppsFeatureEnabled = true var allowApps: Bool var allowAppsChangedHandler: ((Bool) -> Void)? var areAppsPresent = true diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.DarkTheme.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.DarkTheme.png index 5d213ebabbf..610cd8b4224 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.DarkTheme.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.DarkTheme.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc9a761894b19a463a9c5caf35b04c87627af957f6ec2c9407909f2a9a4b1789 -size 221726 +oid sha256:0fd9b27c46142e7c3b92f38938affd5dc5b1c2b3b32f735b94c720851334c844 +size 182304 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.LightTheme.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.LightTheme.png index 1c1bf26a2c7..3be531bbd32 100644 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.LightTheme.png +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationCreationControllerSnapshotTests/testTeamGroupOptions.LightTheme.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da1e1d2438b3d222b051a0e14dd9a3cf68a13525b3013f5fcb204282b981bc5d -size 217805 +oid sha256:2c22298a3a21bfc180bf1d7234c24d193776d199ba2dc1ccff0c6d05f8217bd3 +size 178133 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png deleted file mode 100644 index dc93c02779e..00000000000 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80ec8edf58bcbdb66a7aff42b9a1a6ec7a84d733a5429d2ed295faebcfbeb697 -size 87221 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png deleted file mode 100644 index 0d8dcd8ada9..00000000000 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:36273f048bccc91ea041814bde54270ddc4eaa93501873db11433720e4313da8 -size 88390 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png deleted file mode 100644 index dc93c02779e..00000000000 --- a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:80ec8edf58bcbdb66a7aff42b9a1a6ec7a84d733a5429d2ed295faebcfbeb697 -size 87221 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersItsGroupTitle.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersItsGroupTitle.1.png similarity index 100% rename from wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersItsGroupTitle.1.png rename to wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersItsGroupTitle.1.png diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed.1.png similarity index 100% rename from wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed.1.png rename to wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed.1.png diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed_DarkTheme.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed_DarkTheme.1.png similarity index 100% rename from wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed_DarkTheme.1.png rename to wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreAllowed_DarkTheme.1.png diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png new file mode 100644 index 00000000000..c90f8537ea4 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6610082e9e3f01d30f19ccae2c6720588aeaa228f3af007b21af62c8261dc13a +size 106976 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png new file mode 100644 index 00000000000..06e5663972e --- /dev/null +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItRendersServicesScreenWhenServicesAreNotAllowed_DarkTheme.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e100d329a23de78204c5c2ee4014156c9c5e181a921a9793e7dc163c9362036 +size 106354 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png new file mode 100644 index 00000000000..c90f8537ea4 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.1.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6610082e9e3f01d30f19ccae2c6720588aeaa228f3af007b21af62c8261dc13a +size 106976 diff --git a/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.2.png b/wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.2.png similarity index 100% rename from wire-ios/Wire-iOS Tests/ReferenceImages/ConversationOptionsServicesViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.2.png rename to wire-ios/Wire-iOS Tests/ReferenceImages/ConversationServicesOptionsViewControllerTests/testThatItUpdatesServicesScreenWhenItReceivesAChange.2.png diff --git a/wire-ios/Wire-iOS Tests/StartUIViewControllerSnapshotTests.swift b/wire-ios/Wire-iOS Tests/StartUIViewControllerSnapshotTests.swift index c66e2571358..7c0d361778e 100644 --- a/wire-ios/Wire-iOS Tests/StartUIViewControllerSnapshotTests.swift +++ b/wire-ios/Wire-iOS Tests/StartUIViewControllerSnapshotTests.swift @@ -65,6 +65,7 @@ final class StartUIViewControllerSnapshotTests: CoreDataSnapshotTestCase { func setupSut() { sut = StartUIViewController( + areLegacyBotsAvailable: true, isAppsFeatureEnabled: true, userSession: userSession, mainCoordinator: mockMainCoordinator, diff --git a/wire-ios/Wire-iOS Tests/ZClientViewControllerTests.swift b/wire-ios/Wire-iOS Tests/ZClientViewControllerTests.swift index 7c3832c6f68..e04e9791f9b 100644 --- a/wire-ios/Wire-iOS Tests/ZClientViewControllerTests.swift +++ b/wire-ios/Wire-iOS Tests/ZClientViewControllerTests.swift @@ -37,6 +37,7 @@ final class ZClientViewControllerTests: XCTestCase { userSession.coreDataStack = coreDataFixture.coreDataStack sut = ZClientViewController( account: Account.mockAccount(imageData: mockImageData), + contextProvider: DefaultManagedObjectContextProvider(contextProvider: coreDataFixture.coreDataStack), selfProfileViewsMonitor: MockSelfProfileViewsMonitorImplementation(didViewSelfProfile: false), userSession: userSession, trackingManager: nil, diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index 05cfe25937a..c9fe44e31ac 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -646,10 +646,10 @@ ConversationMessageCell/MockCell.swift, ConversationMessageCell/ReadReceiptSystemMessage/ReadReceiptViewModelTests.swift, ConversationMessageCell/SimpleChatBubblesSnapshotTests.swift, - ConversationOptionsServicesViewControllerTests.swift, ConversationOptionsViewControllerTests.swift, ConversationReplyCellDescriptionTests.swift, ConversationReplyCellTests.swift, + ConversationServicesOptionsViewControllerTests.swift, ConversationStatusTests.swift, "ConversationStatusTests+Icon.swift", ConversationTitleViewTests.swift, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/AddContacts/AddParticipantsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/AddContacts/AddParticipantsViewController.swift index 910f661055c..0616c974416 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/AddContacts/AddParticipantsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/AddContacts/AddParticipantsViewController.swift @@ -369,6 +369,7 @@ final class AddParticipantsViewController: UIViewController { let updated = ConversationCreationValues( isChannel: values.isChannel, isAppsFeatureEnabled: values.isAppsFeatureEnabled, + areLegacyBotsAvailable: values.areLegacyBotsAvailable, name: values.name, participants: userSelection.users, allowGuests: values.allowGuests, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/CellConfiguration.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/CellConfiguration.swift index de92410c175..cd735537648 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/CellConfiguration.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/CellConfiguration.swift @@ -35,6 +35,7 @@ enum CellConfiguration { ) case loading case text(String) + case titleAndBody(title: String, body: String) case info(String) case iconAction( title: String, @@ -65,6 +66,7 @@ enum CellConfiguration { case .leadingButton: ActionCell.self case .loading: LoadingIndicatorCell.self case .text: TextCell.self + case .titleAndBody: TitleBodyCell.self case .info: GuestLinkInfoCell.self case .iconAction: IconActionCell.self case .appearance: SettingsAppearanceCell.self @@ -78,6 +80,7 @@ enum CellConfiguration { .secureLinkHeader, .loading, .text, + .titleAndBody, .info, .appearance: nil case let .leadingButton(_, _, action: action): action @@ -95,6 +98,7 @@ enum CellConfiguration { ActionCell.self, LoadingIndicatorCell.self, TextCell.self, + TitleBodyCell.self, GuestLinkInfoCell.self, IconActionCell.self, SettingsAppearanceCell.self diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift index f06e64a706f..d411de68633 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/Conversation+OptionsConfiguration.swift @@ -31,13 +31,23 @@ extension ZMConversation { private var conversation: ZMConversation private var token: NSObjectProtocol? private let userSession: ZMUserSession + var messageProtocol: MessageProtocol { conversation.messageProtocol } + let areLegacyBotsAvailable: Bool + let isAppsFeatureEnabled: Bool var allowGuestsChangedHandler: ((Bool) -> Void)? var allowAppsChangedHandler: ((Bool) -> Void)? var guestLinkFeatureStatusChangedHandler: ((GuestLinkFeatureStatus) -> Void)? - init(conversation: ZMConversation, userSession: ZMUserSession) { + init( + conversation: ZMConversation, + userSession: ZMUserSession, + areLegacyBotsAvailable: Bool, + isAppsFeatureEnabled: Bool + ) { self.conversation = conversation self.userSession = userSession + self.areLegacyBotsAvailable = areLegacyBotsAvailable + self.isAppsFeatureEnabled = isAppsFeatureEnabled super.init() self.token = ConversationChangeInfo.add(observer: self, for: conversation) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationGuestOptionsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationGuestOptionsViewController.swift index 9d24f97ce86..14b0fe1f928 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationGuestOptionsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationGuestOptionsViewController.swift @@ -43,10 +43,17 @@ final class ConversationGuestOptionsViewController: UIViewController, wr_supportedInterfaceOrientations } - convenience init(conversation: ZMConversation, userSession: ZMUserSession) { + convenience init( + conversation: ZMConversation, + userSession: ZMUserSession, + areLegacyBotsAvailable: Bool, + isAppsFeatureEnabled: Bool + ) { let configuration = ZMConversation.OptionsConfigurationContainer( conversation: conversation, - userSession: userSession + userSession: userSession, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled ) self.init( viewModel: .init( diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewController.swift index 347d0a74bb1..9fbdc1f1a45 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewController.swift @@ -36,10 +36,17 @@ final class ConversationServicesOptionsViewController: UIViewController, wr_supportedInterfaceOrientations } - convenience init(conversation: ZMConversation, userSession: ZMUserSession) { + convenience init( + conversation: ZMConversation, + userSession: ZMUserSession, + areLegacyBotsAvailable: Bool, + isAppsFeatureEnabled: Bool + ) { let configuration = ZMConversation.OptionsConfigurationContainer( conversation: conversation, - userSession: userSession + userSession: userSession, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled ) self.init( viewModel: .init(configuration: configuration) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewModel.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewModel.swift index 88bed73eb12..277e86444ee 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/ConversationServicesOptionsViewModel.swift @@ -17,10 +17,21 @@ // import UIKit +import WireDataModel import WireUtilities protocol ConversationServicesOptionsViewModelConfiguration: AnyObject { + var messageProtocol: MessageProtocol { get } + + /// `true` if at least one bot is whitelisted for the team. + + var areLegacyBotsAvailable: Bool { get } + + /// `true` if the team is able to use apps (feature flag enabled), `false` for individual users or free teams. + + var isAppsFeatureEnabled: Bool { get } + /// `true` if apps can be participants of the conversation, `false` otherwise. var allowApps: Bool { get } @@ -78,10 +89,36 @@ final class ConversationServicesOptionsViewModel { } private func updateRows() { - state.rows = [.allowAppsToggle( - get: { [unowned self] in return configuration.allowApps }, - set: { [unowned self] in setAllowApps($0, sender: $1) } - )] + + var showAppsNotEnabledHint = true + + if configuration.allowApps { + // if apps are already enabled for the conversation, show the toggle + showAppsNotEnabledHint = false + } else if configuration.messageProtocol == .mls, configuration.isAppsFeatureEnabled { + // for MLS conversations consider the apps feature flag + showAppsNotEnabledHint = false + } else if configuration.messageProtocol == .proteus, configuration.areLegacyBotsAvailable { + // for Proteus conversations what matters is if bots are whitelisted for the team + showAppsNotEnabledHint = false + } + + if showAppsNotEnabledHint { + state.rows = [ + .titleAndBody( + title: L10n.Localizable.Conversation.Create.AppsDisabled.title, + body: L10n.Localizable.Conversation.Create.AppsDisabled.message + ) + ] + } else { + state.rows = [ + .allowAppsToggle( + get: { [unowned self] in return configuration.allowApps }, + set: { [unowned self] in setAllowApps($0, sender: $1) } + ) + ] + } + } /// set conversation option AllowApps diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCell.swift new file mode 100644 index 00000000000..0d57346c101 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCell.swift @@ -0,0 +1,45 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI +import WireDesign + +/// Bold headline and normal body text. + +final class TitleBodyCell: UITableViewCell, CellConfigurationConfigurable { + + func configure(with configuration: CellConfiguration) { + guard case let .titleAndBody(title, body) = configuration else { preconditionFailure() } + + contentConfiguration = UIHostingConfiguration { + VStack(alignment: .leading, spacing: 24) { + Text(title) + .bold() + Text(body) + } + } + + backgroundColor = SemanticColors.View.backgroundDefault + + } +} + +@available(iOS 17, *) +#Preview { + TitleBodyCellPreview() +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCellPreview.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCellPreview.swift new file mode 100644 index 00000000000..db565de8c07 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation Options/TitleBodyCellPreview.swift @@ -0,0 +1,67 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import SwiftUI + +final class TitleBodyCellPreview: UITableViewController { + + enum SectionItemIdentifier { + case single + } + + private var dataSource: UITableViewDiffableDataSource! + + override func viewDidLoad() { + super.viewDidLoad() + setupTableView() + loadItems() + } + + private func setupTableView() { + registerCellTypes() + setupDataSource() + tableView.separatorStyle = .none + } + + private func registerCellTypes() { + tableView.register(TitleBodyCell.self, forCellReuseIdentifier: "TitleBodyCell") + } + + private func setupDataSource() { + dataSource = UITableViewDiffableDataSource(tableView: tableView) { tableView, indexPath, _ in + let cell = tableView.dequeueReusableCell(withIdentifier: "TitleBodyCell", for: indexPath) + if let cell = cell as? TitleBodyCell { + cell.configure( + with: .titleAndBody( + title: "Your team doesn't use apps yet", + body: "To improve your workflow with apps, your team needs configuration. Please contact your team admin." + ) + ) + } + return cell + } + } + + private func loadItems() { + var snapshot = dataSource.snapshot() + snapshot.appendSections([.single]) + snapshot.appendItems([.single]) + dataSource.applySnapshotUsingReloadData(snapshot) + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift index 404f97928c5..abf4ec3acc8 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController+ConversationContentViewControllerDelegate.swift @@ -143,6 +143,7 @@ extension ConversationViewController: ConversationContentViewControllerDelegate }) } + @MainActor func conversationContentViewController( _ controller: ConversationContentViewController, presentGuestOptionsFrom sourceView: UIView @@ -152,35 +153,46 @@ extension ConversationViewController: ConversationContentViewControllerDelegate return } - let groupDetailsViewController = GroupDetailsViewController( - conversation: conversation, - userSession: userSession, - mainCoordinator: mainCoordinator, - selfProfileUIBuilder: selfProfileUIBuilder, - conversationCreationRepository: conversationCreationRepository, - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase - ) - let navigationController = UINavigationController(rootViewController: groupDetailsViewController) - groupDetailsViewController.presentGuestOptions(animated: false) - presentParticipantsViewController(navigationController, from: sourceView) + Task { + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false + let isAppsFeatureEnabled = await userSession.clientSessionComponent?.featureConfigRepository + .isFeatureEnabled(.apps) ?? false + + let groupDetailsViewController = GroupDetailsViewController( + conversation: conversation, + userSession: userSession, + mainCoordinator: mainCoordinator, + selfProfileUIBuilder: selfProfileUIBuilder, + conversationCreationRepository: conversationCreationRepository, + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled + ) + let navigationController = UINavigationController(rootViewController: groupDetailsViewController) + groupDetailsViewController.presentGuestOptions(animated: false) + presentParticipantsViewController(navigationController, from: sourceView) + } } + @MainActor func conversationContentViewController( _ controller: ConversationContentViewController, presentParticipantsDetailsWithSelectedUsers selectedUsers: [UserType], from sourceView: UIView ) { - if let groupDetailsViewController = (participantsController as? UINavigationController)? - .topViewController as? GroupDetailsViewController { - groupDetailsViewController.presentParticipantsDetails( - with: conversation.sortedOtherParticipants, - selectedUsers: selectedUsers, - animated: false - ) - } + Task { + if let groupDetailsViewController = (await participantsController as? UINavigationController)? + .topViewController as? GroupDetailsViewController { + groupDetailsViewController.presentParticipantsDetails( + with: conversation.sortedOtherParticipants, + selectedUsers: selectedUsers, + animated: false + ) + } - if let participantsController { - presentParticipantsViewController(participantsController, from: sourceView) + if let participantsController = await participantsController { + presentParticipantsViewController(participantsController, from: sourceView) + } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController.swift index 7f79c17f52a..c7d96751a20 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/ConversationViewController.swift @@ -114,28 +114,36 @@ final class ConversationViewController: UIViewController { var updateLeftNavigationBarItemsTask: Task? var participantsController: UIViewController? { - - var viewController: UIViewController? - - switch conversation.conversationType { - case .group: - viewController = GroupDetailsViewController( - conversation: conversation, - userSession: userSession, - mainCoordinator: mainCoordinator, - selfProfileUIBuilder: selfProfileUIBuilder, - conversationCreationRepository: conversationCreationRepository, - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase - ) - case .`self`, .oneOnOne, .connection: - viewController = createUserDetailViewController() - case .invalid: - fatal("Trying to open invalid conversation") - default: - break + get async { + + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false + let isAppsFeatureEnabled = await userSession.clientSessionComponent?.featureConfigRepository + .isFeatureEnabled(.apps) ?? false + + var viewController: UIViewController? + + switch conversation.conversationType { + case .group: + viewController = GroupDetailsViewController( + conversation: conversation, + userSession: userSession, + mainCoordinator: mainCoordinator, + selfProfileUIBuilder: selfProfileUIBuilder, + conversationCreationRepository: conversationCreationRepository, + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled + ) + case .`self`, .oneOnOne, .connection: + viewController = createUserDetailViewController() + case .invalid: + fatal("Trying to open invalid conversation") + default: + break + } + guard let viewController else { return nil } + return UINavigationController(rootViewController: viewController) } - guard let viewController else { return nil } - return UINavigationController(rootViewController: viewController) } private let individualChangesFactory: MessagesIndividualUpdatesFactory @@ -853,10 +861,13 @@ extension ConversationViewController: ConversationInputBarViewControllerDelegate } } + @MainActor @objc private func onConversationDetailsPressed() { - if let superview = titleView.superview, let participantsController { - presentParticipantsViewController(participantsController, from: superview) + Task { + if let superview = titleView.superview, let participantsController = await participantsController { + presentParticipantsViewController(participantsController, from: superview) + } } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Cells/ConversationCreateAllowAppsCell.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Cells/ConversationCreateAllowAppsCell.swift index e9b0801e418..1d54a4da3ab 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Cells/ConversationCreateAllowAppsCell.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Cells/ConversationCreateAllowAppsCell.swift @@ -40,6 +40,8 @@ final class ConversationCreateAllowAppsCell: IconToggleCell { extension ConversationCreateAllowAppsCell: ConversationCreationValuesConfigurable { func configure(with values: ConversationCreationValues) { isOn = values.allowApps - toggle.isUserInteractionEnabled = values.isAppsFeatureEnabled + toggle.isUserInteractionEnabled = + (values.encryptionProtocol == .mls && values.isAppsFeatureEnabled) || + (values.encryptionProtocol == .proteus && values.areLegacyBotsAvailable) } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift index b099a8035cb..1680a836cfd 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationController.swift @@ -49,6 +49,7 @@ final class ConversationCreationController: UIViewController { typealias CreateGroupName = L10n.Localizable.Conversation.Create.GroupName private let userSession: UserSession + private let areLegacyBotsAvailable: Bool private let collectionViewController = SectionCollectionViewController() @@ -69,7 +70,7 @@ final class ConversationCreationController: UIViewController { private var optionsSections: [ConversationCreateSectionController] { let sections = [ guestsSection, - appsSection, + (values.encryptionProtocol == .mls || areLegacyBotsAvailable) ? appsSection : nil, // TODO: [WPB-16771] Remove conditional when read receipts supported on MLS values.encryptionProtocol != .mls ? receiptsSection : nil, shouldIncludeEncryptionProtocolSection ? encryptionProtocolSection : nil, @@ -173,15 +174,17 @@ final class ConversationCreationController: UIViewController { init( preSelectedParticipants: UserSet?, - userSession: UserSession - ) async { + userSession: UserSession, + isAppsFeatureEnabled: Bool, + areLegacyBotsAvailable: Bool + ) { self.preSelectedParticipants = preSelectedParticipants self.userSession = userSession - let isAppsFeatureEnabled = await userSession.clientSessionComponent?.featureConfigRepository - .isFeatureEnabled(.apps) ?? false + self.areLegacyBotsAvailable = areLegacyBotsAvailable self.values = ConversationCreationValues( isChannel: false, isAppsFeatureEnabled: isAppsFeatureEnabled, + areLegacyBotsAvailable: areLegacyBotsAvailable, encryptionProtocol: userSession.defaultProtocol, selfUser: userSession.selfUser ) @@ -377,7 +380,7 @@ extension ConversationCreationController: AddParticipantsConversationCreationDel let accessMode: [WireNetwork.ConversationAccessMode] = values.allowGuests ? [.invite, .code] : [] let accessRoles = ConversationAccessRoleV2.from( allowGuests: values.allowGuests, - allowApps: values.isAppsFeatureEnabled ? values.allowApps : false + allowApps: (values.isAppsFeatureEnabled || values.areLegacyBotsAvailable) ? values.allowApps : false ).compactMap { $0.toNetworkModel() } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationValues.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationValues.swift index 285c9028dff..48368813514 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationValues.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/ConversationCreationValues.swift @@ -34,6 +34,7 @@ final class ConversationCreationValues { let isChannel: Bool let isAppsFeatureEnabled: Bool + let areLegacyBotsAvailable: Bool var channelHistoryDepth: String? var name: String var allowGuests: Bool @@ -68,6 +69,7 @@ final class ConversationCreationValues { init( isChannel: Bool, isAppsFeatureEnabled: Bool, + areLegacyBotsAvailable: Bool, name: String = "", participants: UserSet = UserSet(), allowGuests: Bool = true, @@ -79,6 +81,7 @@ final class ConversationCreationValues { ) { self.isChannel = isChannel self.isAppsFeatureEnabled = isAppsFeatureEnabled + self.areLegacyBotsAvailable = areLegacyBotsAvailable self.name = name self.unfilteredParticipants = participants self.allowGuests = allowGuests diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/CreateGroupConversationViewControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/CreateGroupConversationViewControllerBuilder.swift index 0268322973c..18caff4192d 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/CreateGroupConversationViewControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/CreateGroupConversationViewControllerBuilder.swift @@ -37,9 +37,14 @@ final class CreateGroupConversationViewControllerBuilder: CreateGroupConversatio @MainActor func build() async -> UIViewController { - let viewController = await ConversationCreationController( + let featureConfigRepository = userSession.clientSessionComponent?.featureConfigRepository + let isAppsFeatureEnabled = await featureConfigRepository?.isFeatureEnabled(.apps) ?? false + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false + let viewController = ConversationCreationController( preSelectedParticipants: nil, - userSession: userSession + userSession: userSession, + isAppsFeatureEnabled: isAppsFeatureEnabled, + areLegacyBotsAvailable: areLegacyBotsAvailable ) viewController.delegate = delegate return viewController diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Sections/ConversationCreateAllowAppsSectionController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Sections/ConversationCreateAllowAppsSectionController.swift index f904b907a7e..4186674fa59 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Sections/ConversationCreateAllowAppsSectionController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/Sections/ConversationCreateAllowAppsSectionController.swift @@ -34,12 +34,20 @@ final class ConversationCreateAllowAppsSectionController: ConversationCreateSect footerText = values.isAppsFeatureEnabled ? L10n.Localizable.Conversation.Create.Apps.subtitle : "" } -} - -extension ConversationCreateAllowAppsSectionController { + /// Returns `1` for showing the toggle only and `2` for showing the disabled toggle with an info banner below. override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - values.isAppsFeatureEnabled ? 1 : 2 + if values.areLegacyBotsAvailable, values.encryptionProtocol == .proteus { + // Whenever the team was using old-style services (bots) we show the toggle but don't depend on the apps + // feature flag. Hence we don't show the banner. + 1 + } else if values.isAppsFeatureEnabled { + // no need to show a banner + 1 + } else { + // disable toggle and show info banner + 2 + } } override func collectionView( diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift index 93c95f02de2..50c027e9f4a 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Conversation/Create/WireConversationChannelCreationFormViewController.swift @@ -63,9 +63,11 @@ final class WireConversationChannelCreationFormViewController: UIViewController self.userSession = userSession let isAppsFeatureEnabled = await userSession.clientSessionComponent?.featureConfigRepository .isFeatureEnabled(.apps) ?? false + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false self.values = ConversationCreationValues( isChannel: true, isAppsFeatureEnabled: isAppsFeatureEnabled, + areLegacyBotsAvailable: areLegacyBotsAvailable, encryptionProtocol: userSession.defaultProtocol, selfUser: userSession.selfUser ) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/GroupDetailsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/GroupDetailsViewController.swift index de731d29c74..f62a3035920 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/GroupDetailsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/GroupDetailsViewController.swift @@ -43,6 +43,9 @@ final class GroupDetailsViewController: UIViewController, ZMConversationObserver private var userStatuses = [UUID: UserStatus]() private let isUserE2EICertifiedUseCase: IsUserE2EICertifiedUseCaseProtocol + private let areLegacyBotsAvailable: Bool + private let isAppsFeatureEnabled: Bool + var didCompleteInitialSync = false { didSet { collectionViewController.sections = computeVisibleSections() } } @@ -57,7 +60,9 @@ final class GroupDetailsViewController: UIViewController, ZMConversationObserver mainCoordinator: AnyMainCoordinator, selfProfileUIBuilder: some SelfProfileViewControllerBuilderProtocol, conversationCreationRepository: any ConversationCreationRepositoryProtocol, - isUserE2EICertifiedUseCase: IsUserE2EICertifiedUseCaseProtocol + isUserE2EICertifiedUseCase: IsUserE2EICertifiedUseCaseProtocol, + areLegacyBotsAvailable: Bool, + isAppsFeatureEnabled: Bool ) { self.conversation = conversation self.userSession = userSession @@ -66,6 +71,8 @@ final class GroupDetailsViewController: UIViewController, ZMConversationObserver self.conversationCreationRepository = conversationCreationRepository self.isUserE2EICertifiedUseCase = isUserE2EICertifiedUseCase self.collectionViewController = SectionCollectionViewController() + self.areLegacyBotsAvailable = areLegacyBotsAvailable + self.isAppsFeatureEnabled = isAppsFeatureEnabled super.init(nibName: nil, bundle: nil) createSubviews() @@ -286,7 +293,8 @@ final class GroupDetailsViewController: UIViewController, ZMConversationObserver conversation: conversation, user: user, delegate: self, - syncCompleted: didCompleteInitialSync + syncCompleted: didCompleteInitialSync, + areLegacyBotsAvailable: areLegacyBotsAvailable ) if optionsSectionController.hasOptions { sections.append(optionsSectionController) @@ -547,14 +555,24 @@ extension GroupDetailsViewController: GroupDetailsSectionControllerDelegate, Gro func presentGuestOptions(animated: Bool) { guard let conversation = conversation as? ZMConversation else { return } guard let userSession = ZMUserSession.shared() else { return } - let menu = ConversationGuestOptionsViewController(conversation: conversation, userSession: userSession) + let menu = ConversationGuestOptionsViewController( + conversation: conversation, + userSession: userSession, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled + ) navigationController?.pushViewController(menu, animated: animated) } func presentServicesOptions(animated: Bool) { guard let conversation = conversation as? ZMConversation else { return } guard let userSession = ZMUserSession.shared() else { return } - let menu = ConversationServicesOptionsViewController(conversation: conversation, userSession: userSession) + let menu = ConversationServicesOptionsViewController( + conversation: conversation, + userSession: userSession, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled + ) navigationController?.pushViewController(menu, animated: animated) } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/Sections/GroupOptionsSectionController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/Sections/GroupOptionsSectionController.swift index bde5c5ac5cd..7f43712132d 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/Sections/GroupOptionsSectionController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/GroupDetails/Sections/GroupOptionsSectionController.swift @@ -40,23 +40,45 @@ final class GroupOptionsSectionController: GroupDetailsSectionController { case timeout case fileCollaboration // keep at the last position + /// Returns `true` if the option is presented to the user or `false` otherwise. + func accessible( in conversation: GroupDetailsConversationType, - by user: UserType + by user: UserType, + areLegacyBotsAvailable: Bool ) -> Bool { switch self { - case .channelAccess: user.canModifyChannelAccessLevelSettings(in: conversation) - case .notifications: user.canModifyNotificationSettings(in: conversation) - case .fileCollaboration: conversation.isCellsEnabled - case .guests: user.canModifyGuestsAccessControlSettings(in: conversation) - case .services: user.canModifyGuestsAccessControlSettings(in: conversation) && conversation - .botCanBeAdded - case .timeout: user.canModifyEphemeralSettings(in: conversation) && !conversation.isCellsEnabled + case .channelAccess: + return user.canModifyChannelAccessLevelSettings(in: conversation) + case .notifications: + return user.canModifyNotificationSettings(in: conversation) + case .fileCollaboration: + return conversation.isCellsEnabled + case .guests: + return user.canModifyGuestsAccessControlSettings(in: conversation) + case .services: + guard user.canModifyGuestsAccessControlSettings(in: conversation), + conversation.botCanBeAdded else { return false } + // if apps are already enabled for a conversation, allow disabling them + if conversation.allowApps { + return true + } + switch conversation.messageProtocol { + case .mls: + // always show the option, but display a hint on the details screen if the feature flag is disabled + return true + case .proteus: + return areLegacyBotsAvailable + default: + return false + } + case .timeout: + return user.canModifyEphemeralSettings(in: conversation) && !conversation.isCellsEnabled case .channelHistoryDepth: if DeveloperFlag.channelsHistory.isOn { - user.canModifyChannelHistoryDepthSettings(in: conversation) + return user.canModifyChannelHistoryDepthSettings(in: conversation) } else { - false + return false } } } @@ -91,12 +113,19 @@ final class GroupOptionsSectionController: GroupDetailsSectionController { conversation: GroupDetailsConversationType, user: UserType, delegate: GroupOptionsSectionControllerDelegate, - syncCompleted: Bool + syncCompleted: Bool, + areLegacyBotsAvailable: Bool ) { self.delegate = delegate self.conversation = conversation self.syncCompleted = syncCompleted - self.options = Option.allCases.filter { $0.accessible(in: conversation, by: user) } + self.options = Option.allCases.filter { option in + option.accessible( + in: conversation, + by: user, + areLegacyBotsAvailable: areLegacyBotsAvailable + ) + } } // MARK: - Collection View diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/DefaultManagedObjectContextProvider.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/DefaultManagedObjectContextProvider.swift new file mode 100644 index 00000000000..814456464b5 --- /dev/null +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/DefaultManagedObjectContextProvider.swift @@ -0,0 +1,34 @@ +// +// Wire +// Copyright (C) 2025 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireData +import WireDataModel + +struct DefaultManagedObjectContextProvider: ManagedObjectContextProvider { + + let contextProvider: any ContextProvider + + var viewContext: NSManagedObjectContext { + contextProvider.viewContext + } + + func newBackgroundContext() -> NSManagedObjectContext { + contextProvider.newBackgroundContext() + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientControllerBuilder.swift index c6594fa46ef..a45e61ab9be 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientControllerBuilder.swift @@ -38,6 +38,7 @@ struct ZClientControllerBuilder { func build(router: AuthenticatedRouterProtocol) -> ZClientViewController { let viewController = ZClientViewController( account: account, + contextProvider: DefaultManagedObjectContextProvider(contextProvider: userSession.contextProvider), selfProfileViewsMonitor: SelfProfileViewsMonitorImplementation(), userSession: userSession, trackingManager: trackingManager, @@ -60,7 +61,7 @@ struct ZClientControllerBuilder { // TODO: [WPB-18798] Temporary fix, when multibackend is on we use new backend environment, when off we use the legacy one accessToken: DefaultAccessTokenProvider(userSession: userSession), fileCache: userSession.fileAssetCache, - contextProvider: DefaultContextProvider(contextProvider: userSession.contextProvider), + contextProvider: DefaultManagedObjectContextProvider(contextProvider: userSession.contextProvider), isFoldersEnabled: DeveloperFlag.wireCellsFolders.isOn, isCollaboraEnabled: DeveloperFlag.wireCellsCollabora.isOn ) @@ -99,17 +100,3 @@ private struct DefaultAccessTokenProvider: AccessTokenProvider { } extension FileAssetCache: WireMessagingDomain.FileCache, @unchecked @retroactive Sendable {} - -private struct DefaultContextProvider: ManagedObjectContextProvider { - - let contextProvider: any ContextProvider - - var viewContext: NSManagedObjectContext { - contextProvider.viewContext - } - - func newBackgroundContext() -> NSManagedObjectContext { - contextProvider.newBackgroundContext() - } - -} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift index 10d80c1987f..42e1952ac27 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/MainController/ZClientViewController.swift @@ -43,6 +43,7 @@ final class ZClientViewController: UIViewController { // MARK: - Private Members - Add wire cells factory here somehow let account: Account + let contextProvider: any ManagedObjectContextProvider let userSession: UserSession let trackingManager: TrackingManager? private let selfProfileViewsMonitor: SelfProfileViewsMonitor @@ -165,7 +166,9 @@ final class ZClientViewController: UIViewController { } ) - private(set) lazy var conversationCreationRepository = ConversationCreationRepository() + private(set) lazy var conversationCreationRepository = ConversationCreationRepository( + searchUsersUseCase: { [weak userSession] in userSession?.makeSearchUsersUseCase() } + ) private(set) lazy var conversationListViewController = ConversationListViewController( account: account, @@ -213,6 +216,7 @@ final class ZClientViewController: UIViewController { required init( account: Account, + contextProvider: any ManagedObjectContextProvider, selfProfileViewsMonitor: SelfProfileViewsMonitor, userSession: UserSession, trackingManager: TrackingManager?, @@ -220,6 +224,7 @@ final class ZClientViewController: UIViewController { wireMessagingFactory: any WireMessagingFactoryProtocol ) { self.account = account + self.contextProvider = contextProvider self.selfProfileViewsMonitor = selfProfileViewsMonitor self.userSession = userSession self.trackingManager = trackingManager @@ -538,17 +543,24 @@ final class ZClientViewController: UIViewController { /// /// - Parameter conversation: conversation to open func openDetailScreen(for conversation: ZMConversation) { - let controller = GroupDetailsViewController( - conversation: conversation, - userSession: userSession, - mainCoordinator: .init(mainCoordinator: mainCoordinator), - selfProfileUIBuilder: selfProfileViewControllerBuilder, - conversationCreationRepository: conversationCreationRepository, - isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase - ) - let navController = UINavigationController(rootViewController: controller) - navController.modalPresentationStyle = .formSheet - present(navController, animated: true) + Task { + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false + let isAppsFeatureEnabled = await userSession.clientSessionComponent?.featureConfigRepository + .isFeatureEnabled(.apps) ?? false + let controller = GroupDetailsViewController( + conversation: conversation, + userSession: userSession, + mainCoordinator: .init(mainCoordinator: mainCoordinator), + selfProfileUIBuilder: selfProfileViewControllerBuilder, + conversationCreationRepository: conversationCreationRepository, + isUserE2EICertifiedUseCase: userSession.isUserE2EICertifiedUseCase, + areLegacyBotsAvailable: areLegacyBotsAvailable, + isAppsFeatureEnabled: isAppsFeatureEnabled + ) + let navController = UINavigationController(rootViewController: controller) + navController.modalPresentationStyle = .formSheet + present(navController, animated: true) + } } @objc @@ -873,7 +885,7 @@ final class ZClientViewController: UIViewController { let useCase = GetUserAccountImageSourceUseCase() cachedAccountImage = try await useCase.invoke( user: userSession.selfUser, - userContext: userSession.contextProvider.viewContext, + userContext: contextProvider.viewContext, account: account ).mapToAccountImageSource() } catch { diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/Services/ServiceDetailViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/Services/ServiceDetailViewController.swift index a55a60fdee8..ad57fe840eb 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/Services/ServiceDetailViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/Services/ServiceDetailViewController.swift @@ -22,7 +22,7 @@ import WireSyncEngine extension ConversationLike where Self: GroupDetailsConversationType { var botCanBeAdded: Bool { - conversationType != .oneOnOne && teamType != nil && allowApps + conversationType != .oneOnOne && teamType != nil } } diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/ConversationCreationRepository.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/ConversationCreationRepository.swift index 4c7e1ceab36..9b0a178d0f9 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/ConversationCreationRepository.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/ConversationCreationRepository.swift @@ -17,5 +17,28 @@ // import WireMessagingDomain +import WireSyncEngine -struct ConversationCreationRepository: ConversationCreationRepositoryProtocol {} +struct ConversationCreationRepository: ConversationCreationRepositoryProtocol { + + let searchUsersUseCase: () -> (any SearchUsersUseCaseProtocol)? + + @concurrent + func areBotsSetUpInTheTeam() async throws -> Bool { + + guard let searchUsersUseCase = searchUsersUseCase() else { return false } + + // search for any old-style services/bots whitelisted in the team + let result = try await searchUsersUseCase.invoke( + query: "", + options: .services, + messageProtocol: .proteus + ) + + return await result.context.perform { + !result.services.isEmpty + } + + } + +} diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift index 4fd120a9e48..32aeee0bd16 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/SearchResultsViewController.swift @@ -22,7 +22,7 @@ import WireSyncEngine enum SearchGroup: Int { case people - case services + case services // TODO: [WPB-20362] consider having apps and bots instead } extension SearchGroup { @@ -166,6 +166,7 @@ final class SearchResultsViewController: UIViewController { }() let servicesSection: SearchServicesSectionController + // TODO: [WPB-20362] add apps section? let inviteTeamMemberSection: InviteTeamMemberSection var isAddingParticipants: Bool @@ -211,11 +212,9 @@ final class SearchResultsViewController: UIViewController { teamMemberAndContactsSection.allowsSelection = isAddingParticipants teamMemberAndContactsSection.selection = userSelection teamMemberAndContactsSection.title = L10n.Localizable.Peoplepicker.Header.contacts - self - .servicesSection = SearchServicesSectionController( - canSelfUserManageTeam: userSession.selfUser - .canManageTeam - ) + self.servicesSection = SearchServicesSectionController( + canSelfUserManageTeam: userSession.selfUser.canManageTeam + ) conversationsSection.title = team != nil ? L10n.Localizable.Peoplepicker.Header .teamConversations(teamName ?? "") : L10n.Localizable.Peoplepicker.Header.conversations self.inviteTeamMemberSection = InviteTeamMemberSection(team: team) diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift index baef181b30d..ebc14a68257 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewController.swift @@ -87,6 +87,11 @@ final class StartUIViewController: UIViewController { let searchResultsViewController: SearchResultsViewController let isAppsFeatureEnabled: Bool + + /// Teams cannot add old-style services (bots) anymore, but teams which have been using bots in the past, they + /// should still be able to start 1:1 conversations with bots. (only if the team's default protocol is Proteus) + let areLegacyBotsAvailable: Bool + let userSession: UserSession let mainCoordinator: AnyMainCoordinator @@ -107,14 +112,23 @@ final class StartUIViewController: UIViewController { searchResultsViewController } + /// Whether there is a switch control for either listing/searching for users/people or apps/bots. + /// + /// The people/apps switch control will only be visible if + /// - apps/bots are not disabled for this build (restricted clients), + /// - the team's default protocol is Proteus the team has been using bots + /// - the team's default protocol is MLS and the `apps` feature flag is enabled. var showsGroupSelector: Bool { - guard DeveloperFlag.considerAppsFeatureFlag.isOn else { - return SearchGroup.all.count > 1 && - userSession.selfUser.canSeeServices && - userSession.defaultProtocol != .mls + guard SearchGroup.all.count > 1, userSession.selfUser.canSeeServices else { return false } + + switch userSession.defaultProtocol { + case .mls: + return isAppsFeatureEnabled + case .proteus: + return areLegacyBotsAvailable + default: + return false } - - return isAppsFeatureEnabled && SearchGroup.all.count > 1 && userSession.selfUser.canSeeServices } // MARK: - Init @@ -124,6 +138,7 @@ final class StartUIViewController: UIViewController { } init( + areLegacyBotsAvailable: Bool, isAppsFeatureEnabled: Bool, userSession: UserSession, mainCoordinator: AnyMainCoordinator, @@ -132,6 +147,7 @@ final class StartUIViewController: UIViewController { selfProfileUIBuilder: SelfProfileViewControllerBuilderProtocol, conversationCreationRepository: any ConversationCreationRepositoryProtocol ) { + self.areLegacyBotsAvailable = areLegacyBotsAvailable self.isAppsFeatureEnabled = isAppsFeatureEnabled self.isFederationEnabled = userSession.resolvedBackendMetadata.isFederationEnabled self.searchResultsViewController = SearchResultsViewController( diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewControllerBuilder.swift b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewControllerBuilder.swift index 308918be846..86e629dfeeb 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewControllerBuilder.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/StartUI/StartUI/StartUIViewControllerBuilder.swift @@ -33,6 +33,8 @@ final class StartUIViewControllerBuilder: ConnectViewControllerBuilderProtocol { let selfProfileUIBuilder: SelfProfileViewControllerBuilderProtocol let conversationCreationRepository: any ConversationCreationRepositoryProtocol + let featureConfigRepository: FeatureConfigRepositoryProtocol + weak var delegate: StartUIDelegate? init( @@ -49,14 +51,16 @@ final class StartUIViewControllerBuilder: ConnectViewControllerBuilderProtocol { self.createGroupConversationUIBuilder = createGroupConversationUIBuilder self.channelConversationFormFactory = channelConversationFormFactory self.selfProfileUIBuilder = selfProfileUIBuilder + self.featureConfigRepository = featureConfigRepository self.conversationCreationRepository = conversationCreationRepository } @MainActor func build() async -> UIViewController { - let featureConfigRepository = userSession.clientSessionComponent?.featureConfigRepository - let isAppsFeatureEnabled = await featureConfigRepository?.isFeatureEnabled(.apps) ?? false + let isAppsFeatureEnabled = await featureConfigRepository.isFeatureEnabled(.apps) + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false let rootViewController = StartUIViewController( + areLegacyBotsAvailable: areLegacyBotsAvailable, isAppsFeatureEnabled: isAppsFeatureEnabled, userSession: userSession, mainCoordinator: mainCoordinator, diff --git a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewController.swift b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewController.swift index 135ecf9d781..17c807035ac 100644 --- a/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewController.swift +++ b/wire-ios/Wire-iOS/Sources/UserInterface/UserProfile/ProfileViewController.swift @@ -147,15 +147,26 @@ final class ProfileViewController: UIViewController { private func bringUpConversationCreationFlow() { Task { - let controller = await ConversationCreationController( + let featureConfigRepository = viewModel.userSession.clientSessionComponent?.featureConfigRepository + let isAppsFeatureEnabled = await featureConfigRepository?.isFeatureEnabled(.apps) ?? false + let areLegacyBotsAvailable = (try? await conversationCreationRepository.areBotsSetUpInTheTeam()) ?? false + let controller = ConversationCreationController( preSelectedParticipants: viewModel.userSet, - userSession: viewModel.userSession + userSession: viewModel.userSession, + isAppsFeatureEnabled: isAppsFeatureEnabled, + areLegacyBotsAvailable: areLegacyBotsAvailable ) controller.delegate = self let wrappedController = controller.wrapInNavigationController() wrappedController.modalPresentationStyle = .formSheet - present(wrappedController, animated: true) + if presentedViewController != nil { + dismiss(animated: true) { + self.present(wrappedController, animated: true) + } + } else { + present(wrappedController, animated: true) + } } }