Skip to content

Commit

Permalink
Create Room with knock rule (#3397)
Browse files Browse the repository at this point in the history
* create knock room implementation (without SDK)

* Apply suggestions from code review

pr suggestions

Co-authored-by: Doug <[email protected]>

* pr suggestions

---------

Co-authored-by: Doug <[email protected]>
  • Loading branch information
Velin92 and pixlwave authored Oct 14, 2024
1 parent 25037a6 commit a16e134
Show file tree
Hide file tree
Showing 17 changed files with 149 additions and 37 deletions.
4 changes: 4 additions & 0 deletions ElementX/Sources/Application/AppSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ final class AppSettings {
case pinningEnabled
case enableOnlySignedDeviceIsolationMode
case identityPinningViolationNotificationsEnabled
case knockingEnabled
}

private static var suiteName: String = InfoPlistReader.main.appGroupIdentifier
Expand Down Expand Up @@ -282,6 +283,9 @@ final class AppSettings {

@UserPreference(key: UserDefaultsKeys.identityPinningViolationNotificationsEnabled, defaultValue: isDevelopmentBuild, storageType: .userDefaults(store))
var identityPinningViolationNotificationsEnabled

@UserPreference(key: UserDefaultsKeys.knockingEnabled, defaultValue: false, storageType: .userDefaults(store))
var knockingEnabled

#endif

Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Mocks/ClientProxyMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ extension ClientProxyMock {
canDeactivateAccount = false
directRoomForUserIDReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
createDirectRoomWithExpectedRoomNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
uploadMediaReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
loadUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
setUserDisplayNameReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
Expand Down
48 changes: 24 additions & 24 deletions ElementX/Sources/Mocks/Generated/GeneratedMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2627,72 +2627,72 @@ class ClientProxyMock: ClientProxyProtocol {
}
//MARK: - createRoom

var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = 0
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount: Int {
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = 0
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount: Int {
get {
if Thread.isMainThread {
return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount
} else {
var returnValue: Int? = nil
DispatchQueue.main.sync {
returnValue = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount
}

return returnValue!
}
}
set {
if Thread.isMainThread {
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = newValue
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue
} else {
DispatchQueue.main.sync {
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingCallsCount = newValue
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingCallsCount = newValue
}
}
}
}
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCalled: Bool {
return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount > 0
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCalled: Bool {
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount > 0
}
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?)?
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?)] = []
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments: (name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)?
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations: [(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?)] = []

var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue: Result<String, ClientProxyError>!
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue: Result<String, ClientProxyError>! {
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue: Result<String, ClientProxyError>!
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue: Result<String, ClientProxyError>! {
get {
if Thread.isMainThread {
return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue
} else {
var returnValue: Result<String, ClientProxyError>? = nil
DispatchQueue.main.sync {
returnValue = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue
returnValue = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue
}

return returnValue!
}
}
set {
if Thread.isMainThread {
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue = newValue
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue
} else {
DispatchQueue.main.sync {
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLUnderlyingReturnValue = newValue
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLUnderlyingReturnValue = newValue
}
}
}
}
var createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure: ((String, String?, Bool, [String], URL?) async -> Result<String, ClientProxyError>)?
var createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure: ((String, String?, Bool, Bool, [String], URL?) async -> Result<String, ClientProxyError>)?

func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLCallsCount += 1
createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, userIDs: userIDs, avatarURL: avatarURL)
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLCallsCount += 1
createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedArguments = (name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL)
DispatchQueue.main.async {
self.createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, userIDs: userIDs, avatarURL: avatarURL))
self.createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReceivedInvocations.append((name: name, topic: topic, isRoomPrivate: isRoomPrivate, isKnockingOnly: isKnockingOnly, userIDs: userIDs, avatarURL: avatarURL))
}
if let createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure {
return await createRoomNameTopicIsRoomPrivateUserIDsAvatarURLClosure(name, topic, isRoomPrivate, userIDs, avatarURL)
if let createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure = createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure {
return await createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLClosure(name, topic, isRoomPrivate, isKnockingOnly, userIDs, avatarURL)
} else {
return createRoomNameTopicIsRoomPrivateUserIDsAvatarURLReturnValue
return createRoomNameTopicIsRoomPrivateIsKnockingOnlyUserIDsAvatarURLReturnValue
}
}
//MARK: - joinRoom
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ final class CreateRoomCoordinator: CoordinatorProtocol {
createRoomParameters: parameters.createRoomParameters,
selectedUsers: parameters.selectedUsers,
analytics: ServiceLocator.shared.analytics,
userIndicatorController: parameters.userIndicatorController)
userIndicatorController: parameters.userIndicatorController,
appSettings: ServiceLocator.shared.settings)
}

func start() {
Expand Down
2 changes: 2 additions & 0 deletions ElementX/Sources/Screens/CreateRoom/CreateRoomModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum CreateRoomViewModelAction {
}

struct CreateRoomViewState: BindableState {
let isKnockingFeatureEnabled: Bool
var selectedUsers: [UserProfileProxy]
var bindings: CreateRoomViewStateBindings
var avatarURL: URL?
Expand All @@ -37,6 +38,7 @@ struct CreateRoomViewStateBindings {
var roomName: String
var roomTopic: String
var isRoomPrivate: Bool
var isKnockingOnly = false
var showAttachmentConfirmationDialog = false

/// Information describing the currently displayed alert.
Expand Down
22 changes: 16 additions & 6 deletions ElementX/Sources/Screens/CreateRoom/CreateRoomViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
createRoomParameters: CurrentValuePublisher<CreateRoomFlowParameters, Never>,
selectedUsers: CurrentValuePublisher<[UserProfileProxy], Never>,
analytics: AnalyticsService,
userIndicatorController: UserIndicatorControllerProtocol) {
userIndicatorController: UserIndicatorControllerProtocol,
appSettings: AppSettings) {
let parameters = createRoomParameters.value

self.userSession = userSession
Expand All @@ -36,7 +37,7 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol

let bindings = CreateRoomViewStateBindings(roomName: parameters.name, roomTopic: parameters.topic, isRoomPrivate: parameters.isRoomPrivate)

super.init(initialViewState: CreateRoomViewState(selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider)
super.init(initialViewState: CreateRoomViewState(isKnockingFeatureEnabled: appSettings.knockingEnabled, selectedUsers: selectedUsers.value, bindings: bindings), mediaProvider: userSession.mediaProvider)

createRoomParameters
.map(\.avatarImageMedia)
Expand Down Expand Up @@ -92,21 +93,28 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
old.roomName == new.roomName && old.roomTopic == new.roomTopic && old.isRoomPrivate == new.isRoomPrivate
}
.sink { [weak self] bindings in
guard let self else { return }
createRoomParameters.name = bindings.roomName
createRoomParameters.topic = bindings.roomTopic
createRoomParameters.isRoomPrivate = bindings.isRoomPrivate
guard let self = self else { return }
updateParameters(bindings: bindings)
actionsSubject.send(.updateDetails(createRoomParameters))
}
.store(in: &cancellables)
}

private func updateParameters(bindings: CreateRoomViewStateBindings) {
createRoomParameters.name = bindings.roomName
createRoomParameters.topic = bindings.roomTopic
createRoomParameters.isRoomPrivate = bindings.isRoomPrivate
createRoomParameters.isKnockingOnly = bindings.isKnockingOnly
}

private func createRoom() async {
defer {
hideLoadingIndicator()
}
showLoadingIndicator()

// Since the parameters are throttled, we need to make sure that the latest values are used
updateParameters(bindings: state.bindings)
let avatarURL: URL?
if let media = createRoomParameters.avatarImageMedia {
switch await userSession.clientProxy.uploadMedia(media) {
Expand Down Expand Up @@ -136,6 +144,8 @@ class CreateRoomViewModel: CreateRoomViewModelType, CreateRoomViewModelProtocol
switch await userSession.clientProxy.createRoom(name: createRoomParameters.name,
topic: createRoomParameters.topic,
isRoomPrivate: createRoomParameters.isRoomPrivate,
// As of right now we don't want to make private rooms with the knock rule
isKnockingOnly: createRoomParameters.isRoomPrivate ? false : createRoomParameters.isKnockingOnly,
userIDs: state.selectedUsers.map(\.userID),
avatarURL: avatarURL) {
case .success(let roomId):
Expand Down
41 changes: 39 additions & 2 deletions ElementX/Sources/Screens/CreateRoom/View/CreateRoomScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ struct CreateRoomScreen: View {
roomSection
topicSection
securitySection
if context.viewState.isKnockingFeatureEnabled,
!context.isRoomPrivate {
roomAccessSection
}
}
.compoundList()
.track(screen: .CreateRoom)
Expand Down Expand Up @@ -151,6 +155,20 @@ struct CreateRoomScreen: View {
}
}

private var roomAccessSection: some View {
Section {
ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionAnyoneOptionTitle,
description: L10n.screenCreateRoomAccessSectionAnyoneOptionDescription),
kind: .selection(isSelected: !context.isKnockingOnly) { context.isKnockingOnly = false })
ListRow(label: .plain(title: L10n.screenCreateRoomAccessSectionKnockingOptionTitle,
description: L10n.screenCreateRoomAccessSectionKnockingOptionDescription),
kind: .selection(isSelected: context.isKnockingOnly) { context.isKnockingOnly = true })
} header: {
Text(L10n.screenCreateRoomAccessSectionHeader.uppercased())
.compoundListSectionHeader()
}
}

private var toolbar: some ToolbarContent {
ToolbarItem(placement: .confirmationAction) {
Button(L10n.actionCreate) {
Expand All @@ -174,7 +192,8 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
createRoomParameters: .init(parameters),
selectedUsers: .init(selectedUsers),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()

static let emtpyViewModel = {
Expand All @@ -184,7 +203,21 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock())
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()

static let publicRoomViewModel = {
let userSession = UserSessionMock(.init(clientProxy: ClientProxyMock(.init(userID: "@userid:example.com"))))
let parameters = CreateRoomFlowParameters(isRoomPrivate: false)
let selectedUsers: [UserProfileProxy] = [.mockAlice, .mockBob, .mockCharlie]
ServiceLocator.shared.settings.knockingEnabled = true
return CreateRoomViewModel(userSession: userSession,
createRoomParameters: .init(parameters),
selectedUsers: .init([]),
analytics: ServiceLocator.shared.analytics,
userIndicatorController: UserIndicatorControllerMock(),
appSettings: ServiceLocator.shared.settings)
}()

static var previews: some View {
Expand All @@ -196,5 +229,9 @@ struct CreateRoom_Previews: PreviewProvider, TestablePreview {
CreateRoomScreen(context: emtpyViewModel.context)
}
.previewDisplayName("Create Room without users")
NavigationStack {
CreateRoomScreen(context: publicRoomViewModel.context)
}
.previewDisplayName("Create Public Room")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ protocol DeveloperOptionsProtocol: AnyObject {
var enableOnlySignedDeviceIsolationMode: Bool { get set }
var elementCallBaseURLOverride: URL? { get set }
var identityPinningViolationNotificationsEnabled: Bool { get set }
var knockingEnabled: Bool { get set }
}

extension AppSettings: DeveloperOptionsProtocol { }
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ struct DeveloperOptionsScreen: View {
}
}

Section("Join rules") {
Toggle(isOn: $context.knockingEnabled) {
Text("Knocking")
Text("Experimental, still using mocked data")
}
}

Section {
Toggle(isOn: $context.enableOnlySignedDeviceIsolationMode) {
Text("Exclude insecure devices when sending/receiving messages")
Expand Down
3 changes: 2 additions & 1 deletion ElementX/Sources/Services/Client/ClientProxy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,8 +365,9 @@ class ClientProxy: ClientProxyProtocol {
}
}

func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError> {
do {
// TODO: Revisit once the SDK supports the knocking API
let parameters = CreateRoomParameters(name: name,
topic: topic,
isEncrypted: isRoomPrivate,
Expand Down
2 changes: 1 addition & 1 deletion ElementX/Sources/Services/Client/ClientProxyProtocol.swift
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ protocol ClientProxyProtocol: AnyObject, MediaLoaderProtocol {

func createDirectRoom(with userID: String, expectedRoomName: String?) async -> Result<String, ClientProxyError>

func createRoom(name: String, topic: String?, isRoomPrivate: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError>
func createRoom(name: String, topic: String?, isRoomPrivate: Bool, isKnockingOnly: Bool, userIDs: [String], avatarURL: URL?) async -> Result<String, ClientProxyError>

func joinRoom(_ roomID: String, via: [String]) async -> Result<Void, ClientProxyError>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ struct CreateRoomFlowParameters {
var name = ""
var topic = ""
var isRoomPrivate = true
var isKnockingOnly = false
var avatarImageMedia: MediaInfo?
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit a16e134

Please sign in to comment.