diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt index b0df2963f7..88ab5e36c6 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/failure/CreateRoomFailure.kt @@ -22,4 +22,7 @@ import org.matrix.android.sdk.api.failure.MatrixError sealed class CreateRoomFailure : Failure.FeatureFailure() { object CreatedWithTimeout : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() + object RoomAliasEmpty: CreateRoomFailure() + object RoomAliasNotAvailable: CreateRoomFailure() + object RoomAliasInvalid: CreateRoomFailure() } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt index 4f0aaf083d..8bf1f077b1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/create/CreateRoomTask.kt @@ -31,8 +31,10 @@ import org.matrix.android.sdk.internal.database.model.RoomEntityFields import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase +import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.network.executeRequest import org.matrix.android.sdk.internal.session.room.RoomAPI +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasDescription import org.matrix.android.sdk.internal.session.room.read.SetReadMarkersTask import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask @@ -45,6 +47,7 @@ internal interface CreateRoomTask : Task internal class DefaultCreateRoomTask @Inject constructor( private val roomAPI: RoomAPI, + @UserId private val userId: String, @SessionDatabase private val monarchy: Monarchy, private val directChatsHelper: DirectChatsHelper, private val updateUserAccountDataTask: UpdateUserAccountDataTask, @@ -61,6 +64,31 @@ internal class DefaultCreateRoomTask @Inject constructor( ?: throw IllegalStateException("You can't create a direct room without an invitedUser") } else null + if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) { + if (params.roomAliasName.isNullOrEmpty()) { + throw CreateRoomFailure.RoomAliasEmpty + } + // Check alias availability + val fullAlias = "#" + params.roomAliasName + ":" + userId.substringAfter(":") + try { + executeRequest(eventBus) { + apiCall = roomAPI.getRoomIdByAlias(fullAlias) + } + } catch (throwable: Throwable) { + if (throwable is Failure.ServerError && throwable.httpCode == 404) { + // This is a 404, so the alias is available: nominal case + null + } else { + // Other error, propagate it + throw throwable + } + } + ?.let { + // Alias already exists: error case + throw CreateRoomFailure.RoomAliasNotAvailable + } + } + val createRoomBody = createRoomBodyBuilder.build(params) val createRoomResponse = try { @@ -68,14 +96,18 @@ internal class DefaultCreateRoomTask @Inject constructor( apiCall = roomAPI.createRoom(createRoomBody) } } catch (throwable: Throwable) { - if (throwable is Failure.ServerError - && throwable.httpCode == 403 - && throwable.error.code == MatrixError.M_FORBIDDEN - && throwable.error.message.startsWith("Federation denied with")) { - throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) - } else { - throw throwable + if (throwable is Failure.ServerError) { + if (throwable.httpCode == 403 + && throwable.error.code == MatrixError.M_FORBIDDEN + && throwable.error.message.startsWith("Federation denied with")) { + throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) + } else if (throwable.httpCode == 400 + && throwable.error.code == MatrixError.M_UNKNOWN + && throwable.error.message == "Invalid characters in room alias") { + throw CreateRoomFailure.RoomAliasInvalid + } } + throw throwable } val roomId = createRoomResponse.roomId // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before) diff --git a/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt b/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt index 08d1332920..0b274cccd8 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormSwitchItem.kt @@ -16,7 +16,9 @@ package im.vector.app.features.form +import android.view.View import android.widget.TextView +import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass import com.google.android.material.switchmaterial.SwitchMaterial @@ -43,6 +45,9 @@ abstract class FormSwitchItem : VectorEpoxyModel() { @EpoxyAttribute var summary: String? = null + @EpoxyAttribute + var showDivider: Boolean = true + override fun bind(holder: Holder) { super.bind(holder) holder.view.setOnClickListener { @@ -61,6 +66,7 @@ abstract class FormSwitchItem : VectorEpoxyModel() { holder.switchView.setOnCheckedChangeListener { _, isChecked -> listener?.invoke(isChecked) } + holder.divider.isVisible = showDivider } override fun shouldSaveViewState(): Boolean { @@ -77,5 +83,6 @@ abstract class FormSwitchItem : VectorEpoxyModel() { val titleView by bind(R.id.formSwitchTitle) val summaryView by bind(R.id.formSwitchSummary) val switchView by bind(R.id.formSwitchSwitch) + val divider by bind(R.id.formSwitchDivider) } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt index 61799a741f..b50c56e1db 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomAction.kt @@ -24,7 +24,7 @@ sealed class CreateRoomAction : VectorViewModelAction { data class SetName(val name: String) : CreateRoomAction() data class SetTopic(val topic: String) : CreateRoomAction() data class SetIsPublic(val isPublic: Boolean) : CreateRoomAction() - data class SetIsInRoomDirectory(val isInRoomDirectory: Boolean) : CreateRoomAction() + data class SetRoomAliasLocalPart(val aliasLocalPart: String) : CreateRoomAction() data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction() object ToggleShowAdvanced : CreateRoomAction() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt index 157e192668..3f86d1adca 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomController.kt @@ -32,6 +32,7 @@ import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSwitchItem +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import javax.inject.Inject class CreateRoomController @Inject constructor(private val stringProvider: StringProvider, @@ -61,6 +62,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin is Fail -> { // display the form buildForm(viewState, true) + // TODO BMA DO NOT COMMIT Update this errorWithRetryItem { id("error") text(errorFormatter.toHumanReadable(asyncCreateRoom.error)) @@ -115,21 +117,32 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin enabled(enableFormElement) title(stringProvider.getString(R.string.create_room_public_title)) summary(stringProvider.getString(R.string.create_room_public_description)) - switchChecked(viewState.isPublic) + switchChecked(viewState.roomType is CreateRoomViewState.RoomType.Public) + showDivider(viewState.roomType !is CreateRoomViewState.RoomType.Public) listener { value -> listener?.setIsPublic(value) } } - formSwitchItem { - id("directory") - enabled(enableFormElement) - title(stringProvider.getString(R.string.create_room_directory_title)) - summary(stringProvider.getString(R.string.create_room_directory_description)) - switchChecked(viewState.isInRoomDirectory) - - listener { value -> - listener?.setIsInRoomDirectory(value) + // Room alias + if (viewState.roomType is CreateRoomViewState.RoomType.Public) { + roomAliasEditItem { + id("alias") + enabled(enableFormElement) + value(viewState.roomType.aliasLocalPart) + homeServer(":" + viewState.homeServerName) + errorMessage( + when ((viewState.asyncCreateRoomRequest as? Fail)?.error) { + is CreateRoomFailure.RoomAliasEmpty -> R.string.create_room_alias_empty + is CreateRoomFailure.RoomAliasNotAvailable -> R.string.create_room_alias_already_in_use + is CreateRoomFailure.RoomAliasInvalid -> R.string.create_room_alias_invalid + else -> null + } + ?.let { stringProvider.getString(it) } + ) + onTextChange { value -> + listener?.setAliasLocalPart(value) + } } } formSwitchItem { @@ -162,6 +175,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin title(stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName)) summary(stringProvider.getString(R.string.create_room_disable_federation_description)) switchChecked(viewState.disableFederation) + showDivider(false) listener { value -> listener?.setDisableFederation(value) } } } @@ -179,7 +193,7 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin fun onNameChange(newName: String) fun onTopicChange(newTopic: String) fun setIsPublic(isPublic: Boolean) - fun setIsInRoomDirectory(isInRoomDirectory: Boolean) + fun setAliasLocalPart(aliasLocalPart: String) fun setIsEncrypted(isEncrypted: Boolean) fun retry() fun toggleShowAdvanced() diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt index 729c97370c..124b6a415a 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomFragment.kt @@ -46,6 +46,7 @@ class CreateRoomFragment @Inject constructor( OnBackPressed { private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel + // TODO BMA: use fragmentViewMode(). Else back does not reset the form. Use Fragment Argument to pass room name private val viewModel: CreateRoomViewModel by activityViewModel() private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) @@ -102,8 +103,8 @@ class CreateRoomFragment @Inject constructor( viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) } - override fun setIsInRoomDirectory(isInRoomDirectory: Boolean) { - viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory)) + override fun setAliasLocalPart(aliasLocalPart: String) { + viewModel.handle(CreateRoomAction.SetRoomAliasLocalPart(aliasLocalPart)) } override fun setIsEncrypted(isEncrypted: Boolean) { diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt index fcb98916cc..52e1b3f801 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewModel.kt @@ -77,7 +77,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr setState { copy( - isEncrypted = !isPublic && adminE2EByDefault, + isEncrypted = roomType is CreateRoomViewState.RoomType.Private && adminE2EByDefault, hsAdminHasDisabledE2E = !adminE2EByDefault ) } @@ -100,16 +100,16 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr override fun handle(action: CreateRoomAction) { when (action) { - is CreateRoomAction.SetAvatar -> setAvatar(action) - is CreateRoomAction.SetName -> setName(action) - is CreateRoomAction.SetTopic -> setTopic(action) - is CreateRoomAction.SetIsPublic -> setIsPublic(action) - is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action) - is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) - is CreateRoomAction.Create -> doCreateRoom() - CreateRoomAction.Reset -> doReset() - CreateRoomAction.ToggleShowAdvanced -> toggleShowAdvanced() - is CreateRoomAction.DisableFederation -> disableFederation(action) + is CreateRoomAction.SetAvatar -> setAvatar(action) + is CreateRoomAction.SetName -> setName(action) + is CreateRoomAction.SetTopic -> setTopic(action) + is CreateRoomAction.SetIsPublic -> setIsPublic(action) + is CreateRoomAction.SetRoomAliasLocalPart -> setRoomAliasLocalPart(action) + is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) + is CreateRoomAction.Create -> doCreateRoom() + CreateRoomAction.Reset -> doReset() + CreateRoomAction.ToggleShowAdvanced -> toggleShowAdvanced() + is CreateRoomAction.DisableFederation -> disableFederation(action) }.exhaustive } @@ -150,13 +150,31 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) } private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { - copy( - isPublic = action.isPublic, - isEncrypted = !action.isPublic && adminE2EByDefault - ) + if (action.isPublic) { + copy( + roomType = CreateRoomViewState.RoomType.Public(""), + isEncrypted = false + ) + } else { + copy( + roomType = CreateRoomViewState.RoomType.Private, + isEncrypted = adminE2EByDefault + ) + } } - private fun setIsInRoomDirectory(action: CreateRoomAction.SetIsInRoomDirectory) = setState { copy(isInRoomDirectory = action.isInRoomDirectory) } + private fun setRoomAliasLocalPart(action: CreateRoomAction.SetRoomAliasLocalPart) { + withState { state -> + if (state.roomType is CreateRoomViewState.RoomType.Public) { + setState { + copy( + roomType = CreateRoomViewState.RoomType.Public(action.aliasLocalPart) + ) + } + } + } + // Else ignore + } private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) } @@ -174,10 +192,21 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr name = state.roomName.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() } avatarUri = state.avatarUri - // Directory visibility - visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE - // Public room - preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT + when (state.roomType) { + is CreateRoomViewState.RoomType.Public -> { + // Directory visibility + visibility = RoomDirectoryVisibility.PUBLIC + // Preset + preset = CreateRoomPreset.PRESET_PUBLIC_CHAT + roomAliasName = state.roomType.aliasLocalPart + } + is CreateRoomViewState.RoomType.Private -> { + // Directory visibility + visibility = RoomDirectoryVisibility.PRIVATE + // Preset + preset = CreateRoomPreset.PRESET_PRIVATE_CHAT + } + }.exhaustive // Disabling federation disableFederation = state.disableFederation diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt index d1e5c0b1bd..cc7a441eb5 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/CreateRoomViewState.kt @@ -25,8 +25,7 @@ data class CreateRoomViewState( val avatarUri: Uri? = null, val roomName: String = "", val roomTopic: String = "", - val isPublic: Boolean = false, - val isInRoomDirectory: Boolean = false, + val roomType: RoomType = RoomType.Private, val isEncrypted: Boolean = false, val showAdvanced: Boolean = false, val disableFederation: Boolean = false, @@ -39,4 +38,9 @@ data class CreateRoomViewState( * Return true if there is not important input from user */ fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty() + + sealed class RoomType { + object Private : RoomType() + data class Public(val aliasLocalPart: String) : RoomType() + } } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt new file mode 100644 index 0000000000..041a5c5c51 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt @@ -0,0 +1,89 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.features.roomdirectory.createroom + +import android.text.Editable +import android.view.View +import android.widget.TextView +import androidx.core.view.isVisible +import com.airbnb.epoxy.EpoxyAttribute +import com.airbnb.epoxy.EpoxyModelClass +import com.google.android.material.textfield.TextInputEditText +import com.google.android.material.textfield.TextInputLayout +import im.vector.app.R +import im.vector.app.core.epoxy.VectorEpoxyHolder +import im.vector.app.core.epoxy.VectorEpoxyModel +import im.vector.app.core.platform.SimpleTextWatcher + +@EpoxyModelClass(layout = R.layout.item_room_alias_text_input) +abstract class RoomAliasEditItem : VectorEpoxyModel() { + + @EpoxyAttribute + var value: String? = null + + @EpoxyAttribute + var showBottomSeparator: Boolean = true + + @EpoxyAttribute + var errorMessage: String? = null + + @EpoxyAttribute + var homeServer: String? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var onTextChange: ((String) -> Unit)? = null + + private val onTextChangeListener = object : SimpleTextWatcher() { + override fun afterTextChanged(s: Editable) { + onTextChange?.invoke(s.toString()) + } + } + + override fun bind(holder: Holder) { + super.bind(holder) + holder.textInputLayout.isEnabled = enabled + holder.textInputLayout.error = errorMessage + + // Update only if text is different and value is not null + if (value != null && holder.textInputEditText.text.toString() != value) { + holder.textInputEditText.setText(value) + } + holder.textInputEditText.isEnabled = enabled + holder.textInputEditText.addTextChangedListener(onTextChangeListener) + holder.homeServerText.text = homeServer + holder.bottomSeparator.isVisible = showBottomSeparator + } + + override fun shouldSaveViewState(): Boolean { + return false + } + + override fun unbind(holder: Holder) { + super.unbind(holder) + holder.textInputEditText.removeTextChangedListener(onTextChangeListener) + } + + class Holder : VectorEpoxyHolder() { + val textInputLayout by bind(R.id.itemRoomAliasTextInputLayout) + val textInputEditText by bind(R.id.itemRoomAliasTextInputEditText) + val homeServerText by bind(R.id.itemRoomAliasHomeServer) + val bottomSeparator by bind(R.id.itemRoomAliasDivider) + } +} diff --git a/vector/src/main/res/layout/item_room_alias_text_input.xml b/vector/src/main/res/layout/item_room_alias_text_input.xml new file mode 100644 index 0000000000..44c76b631d --- /dev/null +++ b/vector/src/main/res/layout/item_room_alias_text_input.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 5768750021..ee8c3f7da8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -2100,6 +2100,11 @@ Block anyone not part of %s from ever joining this room You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later. + Room address + This address is already in use + Please provide a room address + Some characters are not allowed + Your email domain is not authorized to register on this server Untrusted sign in