diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt index 841e833271..7f5f3f54ef 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt @@ -157,4 +157,10 @@ object MatrixPatterns { fun isValidOrderString(order: String?) : Boolean { return order != null && order.length < 50 && order matches ORDER_STRING_REGEX } + + fun candidateAliasFromRoomName(name: String): String { + return Regex("\\s").replace(name.lowercase(), "_").let { + "[^a-z0-9._%#@=+-]".toRegex().replace(it, "") + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt new file mode 100644 index 0000000000..6f607569c0 --- /dev/null +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/AliasAvailabilityResult.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Matrix.org Foundation C.I.C. + * + * 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 org.matrix.android.sdk.api.session.room + +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError + +sealed class AliasAvailabilityResult { + object Available: AliasAvailabilityResult() + data class NotAvailable(val roomAliasError: RoomAliasError) : AliasAvailabilityResult() +} diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt index 176de8e408..f3e3913bc1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/RoomDirectoryService.kt @@ -40,4 +40,6 @@ interface RoomDirectoryService { * Set the visibility of a room in the directory */ suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) + + suspend fun checkAliasAvailability(aliasLocalPart: String?) : AliasAvailabilityResult } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt index fedf38fe06..e5288e4045 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/SpaceService.kt @@ -36,7 +36,11 @@ interface SpaceService { /** * Just a shortcut for space creation for ease of use */ - suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String + suspend fun createSpace(name: String, + topic: String?, + avatarUri: Uri?, + isPublic: Boolean, + roomAliasLocalPart: String? = null): String /** * Get a space from a roomId diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt index 218d846afb..7330c91c20 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/DefaultRoomDirectoryService.kt @@ -16,10 +16,13 @@ package org.matrix.android.sdk.internal.session.room +import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult import org.matrix.android.sdk.api.session.room.RoomDirectoryService +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsParams import org.matrix.android.sdk.api.session.room.model.roomdirectory.PublicRoomsResponse +import org.matrix.android.sdk.internal.session.room.alias.RoomAliasAvailabilityChecker import org.matrix.android.sdk.internal.session.room.directory.GetPublicRoomTask import org.matrix.android.sdk.internal.session.room.directory.GetRoomDirectoryVisibilityTask import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVisibilityTask @@ -28,7 +31,8 @@ import javax.inject.Inject internal class DefaultRoomDirectoryService @Inject constructor( private val getPublicRoomTask: GetPublicRoomTask, private val getRoomDirectoryVisibilityTask: GetRoomDirectoryVisibilityTask, - private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask + private val setRoomDirectoryVisibilityTask: SetRoomDirectoryVisibilityTask, + private val roomAliasAvailabilityChecker: RoomAliasAvailabilityChecker ) : RoomDirectoryService { override suspend fun getPublicRooms(server: String?, @@ -43,4 +47,13 @@ internal class DefaultRoomDirectoryService @Inject constructor( override suspend fun setRoomDirectoryVisibility(roomId: String, roomDirectoryVisibility: RoomDirectoryVisibility) { setRoomDirectoryVisibilityTask.execute(SetRoomDirectoryVisibilityTask.Params(roomId, roomDirectoryVisibility)) } + + override suspend fun checkAliasAvailability(aliasLocalPart: String?): AliasAvailabilityResult { + return try { + roomAliasAvailabilityChecker.check(aliasLocalPart) + AliasAvailabilityResult.Available + } catch (failure: RoomAliasError) { + AliasAvailabilityResult.NotAvailable(failure) + } + } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt index b39cbaa582..66164c5280 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/alias/RoomAliasAvailabilityChecker.kt @@ -51,13 +51,13 @@ internal class RoomAliasAvailabilityChecker @Inject constructor( } catch (throwable: Throwable) { if (throwable is Failure.ServerError && throwable.httpCode == 404) { // This is a 404, so the alias is available: nominal case - null + return } else { // Other error, propagate it throw throwable } } - ?.let { + .let { // Alias already exists: error case throw RoomAliasError.AliasNotAvailable } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index 9c6153b349..0c5c0416f9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -66,12 +66,13 @@ internal class DefaultSpaceService @Inject constructor( return createRoomTask.executeRetry(params, 3) } - override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String { + override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean, roomAliasLocalPart: String?): String { return createSpace(CreateSpaceParams().apply { this.name = name this.topic = topic this.avatarUri = avatarUri if (isPublic) { + this.roomAliasName = roomAliasLocalPart this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy( invite = 0 ) diff --git a/newsfragment/3483.feature b/newsfragment/3483.feature new file mode 100644 index 0000000000..e6c27992ea --- /dev/null +++ b/newsfragment/3483.feature @@ -0,0 +1 @@ +Add option to set aliases for public spaces \ No newline at end of file diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index e2203057ab..2b2fddd0c9 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -17,6 +17,7 @@ package im.vector.app.features.form import android.text.Editable +import android.view.View import android.view.inputmethod.EditorInfo import android.widget.TextView import com.airbnb.epoxy.EpoxyAttribute @@ -40,6 +41,9 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var value: String? = null + @EpoxyAttribute + var forceUpdateValue: Boolean = false + @EpoxyAttribute var errorMessage: String? = null @@ -64,12 +68,23 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var editorActionListener: TextView.OnEditorActionListener? = null + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) + var onFocusChange: ((Boolean) -> Unit)? = null + + @EpoxyAttribute + var prefixText: String? = null + + @EpoxyAttribute + var suffixText: String? = null + private val onTextChangeListener = object : SimpleTextWatcher() { override fun afterTextChanged(s: Editable) { onTextChange?.invoke(s.toString()) } } + private val onFocusChangedListener = View.OnFocusChangeListener { _, hasFocus -> onFocusChange?.invoke(hasFocus) } + override fun bind(holder: Holder) { super.bind(holder) holder.textInputLayout.isEnabled = enabled @@ -77,7 +92,14 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputLayout.error = errorMessage holder.textInputLayout.endIconMode = endIconMode ?: TextInputLayout.END_ICON_NONE - holder.setValueOnce(holder.textInputEditText, value) + holder.textInputLayout.prefixText = prefixText + holder.textInputLayout.suffixText = suffixText + + if (forceUpdateValue) { + holder.textInputEditText.setText(value) + } else { + holder.setValueOnce(holder.textInputEditText, value) + } holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } @@ -86,6 +108,7 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputEditText.addTextChangedListenerOnce(onTextChangeListener) holder.textInputEditText.setOnEditorActionListener(editorActionListener) + holder.textInputEditText.onFocusChangeListener = onFocusChangedListener } override fun shouldSaveViewState(): Boolean { 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 61b34a1499..6c441c355c 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 @@ -99,11 +99,13 @@ class CreateRoomController @Inject constructor( } if (viewState.roomVisibilityType is CreateRoomViewState.RoomVisibilityType.Public) { // Room alias for public room - roomAliasEditItem { + formEditTextItem { id("alias") enabled(enableFormElement) value(viewState.roomVisibilityType.aliasLocalPart) - homeServer(":" + viewState.homeServerName) + suffixText(":" + viewState.homeServerName) + prefixText("#") + hint(host.stringProvider.getString(R.string.room_alias_address_hint)) errorMessage( host.roomAliasErrorFormatter.format( (((viewState.asyncCreateRoomRequest as? Fail)?.error) as? CreateRoomFailure.AliasError)?.aliasError) 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 deleted file mode 100644 index 9cb6f79a56..0000000000 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.widget.TextView -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.TextListener -import im.vector.app.core.epoxy.VectorEpoxyHolder -import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.epoxy.addTextChangedListenerOnce -import im.vector.app.core.epoxy.setValueOnce -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 errorMessage: String? = null - - @EpoxyAttribute - var homeServer: String? = null - - @EpoxyAttribute - var enabled: Boolean = true - - @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) - var onTextChange: TextListener? = 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 - - holder.setValueOnce(holder.textInputEditText, value) - holder.textInputEditText.isEnabled = enabled - holder.textInputEditText.addTextChangedListenerOnce(onTextChangeListener) - holder.homeServerText.text = homeServer - } - - 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) - } -} diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt index 97050e9c6d..4a683b6292 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/alias/RoomAliasController.kt @@ -36,9 +36,9 @@ import im.vector.app.features.discovery.settingsInfoItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formSwitchItem import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter -import im.vector.app.features.roomdirectory.createroom.roomAliasEditItem import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomType import javax.inject.Inject class RoomAliasController @Inject constructor( @@ -71,7 +71,9 @@ class RoomAliasController @Inject constructor( // Published alias buildPublishInfo(data) // Room directory visibility - buildRoomDirectoryVisibility(data) + if (data.roomSummary.invoke()?.roomType != RoomType.SPACE) { + buildRoomDirectoryVisibility(data) + } // Local alias buildLocalInfo(data) } @@ -243,10 +245,12 @@ class RoomAliasController @Inject constructor( } } is RoomAliasViewState.AddAliasState.Editing -> { - roomAliasEditItem { + formEditTextItem { id("newLocalAlias") value(data.newLocalAliasState.value) - homeServer(":" + data.homeServerName) + suffixText(":" + data.homeServerName) + prefixText("#") + hint(host.stringProvider.getString(R.string.room_alias_address_hint)) errorMessage(host.roomAliasErrorFormatter.format((data.newLocalAliasState.asyncRequest as? Fail)?.error as? RoomAliasError)) onTextChange { value -> host.callback?.setNewLocalAliasLocalPart(value) diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt index 6bf31dd5ce..a02755a155 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -113,6 +113,9 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac CreateSpaceEvents.HideModalLoading -> { hideWaitingView() } + is CreateSpaceEvents.ShowModalLoading -> { + showWaitingView(it.message) + } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt index cd31b40354..1f0ed6428f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceAction.kt @@ -23,6 +23,7 @@ sealed class CreateSpaceAction : VectorViewModelAction { data class SetRoomType(val type: SpaceType) : CreateSpaceAction() data class NameChanged(val name: String) : CreateSpaceAction() data class TopicChanged(val topic: String) : CreateSpaceAction() + data class SpaceAliasChanged(val aliasLocalPart: String) : CreateSpaceAction() data class SetAvatar(val uri: Uri?) : CreateSpaceAction() object OnBackPressed : CreateSpaceAction() object NextFromDetails : CreateSpaceAction() diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt index b5925eec59..544c33948b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt @@ -84,6 +84,10 @@ class CreateSpaceDetailsFragment @Inject constructor( sharedViewModel.handle(CreateSpaceAction.TopicChanged(newTopic)) } + override fun setAliasLocalPart(aliasLocalPart: String) { + sharedViewModel.handle(CreateSpaceAction.SpaceAliasChanged(aliasLocalPart)) + } + override fun onBackPressed(toolbarButton: Boolean): Boolean { sharedViewModel.handle(CreateSpaceAction.OnBackPressed) return true diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt index c3fa2b2068..073531353f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceEvents.kt @@ -27,4 +27,5 @@ sealed class CreateSpaceEvents : VectorViewEvents { data class FinishSuccess(val spaceId: String, val defaultRoomId: String?, val topology: SpaceTopology?) : CreateSpaceEvents() data class ShowModalError(val errorMessage: String) : CreateSpaceEvents() object HideModalLoading : CreateSpaceEvents() + data class ShowModalLoading(val message: String?) : CreateSpaceEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt index 147fd3a616..39a69e837b 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceState.kt @@ -28,6 +28,10 @@ data class CreateSpaceState( val step: Step = Step.ChooseType, val spaceType: SpaceType? = null, val spaceTopology: SpaceTopology? = null, + val homeServerName: String? = null, + val aliasLocalPart: String? = null, + val aliasManuallyModified: Boolean = false, + val aliasVerificationTask: Async = Uninitialized, val nameInlineError: String? = null, val defaultRooms: Map? = null, val creationResult: Async = Uninitialized diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt index aff342cea7..60110b7dd5 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -35,14 +35,27 @@ import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.MatrixPatterns +import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult +import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure class CreateSpaceViewModel @AssistedInject constructor( @Assisted initialState: CreateSpaceState, + private val session: Session, private val stringProvider: StringProvider, private val createSpaceViewModelTask: CreateSpaceViewModelTask, private val errorFormatter: ErrorFormatter ) : VectorViewModel(initialState) { + init { + setState { + copy( + homeServerName = session.myUserId.substringAfter(":") + ) + } + } + @AssistedFactory interface Factory { fun create(initialState: CreateSpaceState): CreateSpaceViewModel @@ -81,10 +94,22 @@ class CreateSpaceViewModel @AssistedInject constructor( } is CreateSpaceAction.NameChanged -> { setState { - copy( - nameInlineError = null, - name = action.name - ) + if (aliasManuallyModified) { + copy( + nameInlineError = null, + name = action.name, + aliasVerificationTask = Uninitialized + ) + } else { + val tentativeAlias = + MatrixPatterns.candidateAliasFromRoomName(action.name) + copy( + nameInlineError = null, + name = action.name, + aliasLocalPart = tentativeAlias, + aliasVerificationTask = Uninitialized + ) + } } } is CreateSpaceAction.TopicChanged -> { @@ -94,6 +119,17 @@ class CreateSpaceViewModel @AssistedInject constructor( ) } } + is CreateSpaceAction.SpaceAliasChanged -> { + // This called only when the alias is change manually + // not when programmatically changed via a change on name + setState { + copy( + aliasManuallyModified = true, + aliasLocalPart = action.aliasLocalPart, + aliasVerificationTask = Uninitialized + ) + } + } CreateSpaceAction.OnBackPressed -> { handleBackNavigation() } @@ -204,12 +240,38 @@ class CreateSpaceViewModel @AssistedInject constructor( } _viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType) } else { + // it'a public space, let's check alias + val aliasLocalPart = state.aliasLocalPart + _viewEvents.post(CreateSpaceEvents.ShowModalLoading(null)) setState { - copy( - step = CreateSpaceState.Step.AddRooms - ) + copy(aliasVerificationTask = Loading()) + } + viewModelScope.launch { + try { + when (val result = session.checkAliasAvailability(aliasLocalPart)) { + AliasAvailabilityResult.Available -> { + setState { + copy( + step = CreateSpaceState.Step.AddRooms + ) + } + _viewEvents.post(CreateSpaceEvents.HideModalLoading) + _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) + } + is AliasAvailabilityResult.NotAvailable -> { + setState { + copy(aliasVerificationTask = Fail(result.roomAliasError)) + } + _viewEvents.post(CreateSpaceEvents.HideModalLoading) + } + } + } catch (failure: Throwable) { + setState { + copy(aliasVerificationTask = Fail(failure)) + } + _viewEvents.post(CreateSpaceEvents.HideModalLoading) + } } - _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) } } } @@ -221,6 +283,9 @@ class CreateSpaceViewModel @AssistedInject constructor( } viewModelScope.launch(Dispatchers.IO) { try { + val alias = if (state.spaceType == SpaceType.Public) { + state.aliasLocalPart + } else null val result = createSpaceViewModelTask.execute( CreateSpaceTaskParams( spaceName = spaceName, @@ -230,7 +295,8 @@ class CreateSpaceViewModel @AssistedInject constructor( defaultRooms = state.defaultRooms ?.entries ?.sortedBy { it.key } - ?.mapNotNull { it.value } ?: emptyList() + ?.mapNotNull { it.value } ?: emptyList(), + spaceAlias = alias ) ) when (result) { @@ -260,10 +326,22 @@ class CreateSpaceViewModel @AssistedInject constructor( ) } is CreateSpaceTaskResult.FailedToCreateSpace -> { - setState { - copy(creationResult = Fail(result.failure)) + if (result.failure is CreateRoomFailure.AliasError) { + setState { + copy( + step = CreateSpaceState.Step.SetDetails, + aliasVerificationTask = Fail(result.failure.aliasError), + creationResult = Uninitialized + ) + } + _viewEvents.post(CreateSpaceEvents.HideModalLoading) + _viewEvents.post(CreateSpaceEvents.NavigateToDetails) + } else { + setState { + copy(creationResult = Fail(result.failure)) + } + _viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(result.failure))) } - _viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(result.failure))) } } } catch (failure: Throwable) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt index 9ce1089f6c..f1731caf76 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModelTask.kt @@ -45,6 +45,7 @@ data class CreateSpaceTaskParams( val spaceName: String, val spaceTopic: String?, val spaceAvatar: Uri? = null, + val spaceAlias: String? = null, val isPublic: Boolean, val defaultRooms: List = emptyList() ) @@ -57,7 +58,13 @@ class CreateSpaceViewModelTask @Inject constructor( override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult { val spaceID = try { - session.spaceService().createSpace(params.spaceName, params.spaceTopic, params.spaceAvatar, params.isPublic) + session.spaceService().createSpace( + params.spaceName, + params.spaceTopic, + params.spaceAvatar, + params.isPublic, + params.spaceAlias + ) } catch (failure: Throwable) { return CreateSpaceTaskResult.FailedToCreateSpace(failure) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt index 9b3686c513..27c08d1f6f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -17,24 +17,39 @@ package im.vector.app.features.spaces.create import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail import im.vector.app.R +import im.vector.app.core.epoxy.TextListener import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditableSquareAvatarItem import im.vector.app.features.form.formMultiLineEditTextItem import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomdirectory.createroom.RoomAliasErrorFormatter +import org.matrix.android.sdk.api.session.room.alias.RoomAliasError import org.matrix.android.sdk.api.util.MatrixItem import javax.inject.Inject class SpaceDetailEpoxyController @Inject constructor( private val stringProvider: StringProvider, - private val avatarRenderer: AvatarRenderer + private val avatarRenderer: AvatarRenderer, + private val roomAliasErrorFormatter: RoomAliasErrorFormatter ) : TypedEpoxyController() { var listener: Listener? = null -// var shouldForceFocusOnce = true + /** + * Alias text can be automatically set when changing the room name, + * We have to be able to make a difference between a programming change versus + * a user change. + */ + var aliasTextIsFocused = false + private val aliasTextWatcher: TextListener = { + if (aliasTextIsFocused) { + listener?.setAliasLocalPart(it) + } + } override fun buildModels(data: CreateSpaceState?) { val host = this @@ -65,20 +80,31 @@ class SpaceDetailEpoxyController @Inject constructor( value(data?.name) hint(host.stringProvider.getString(R.string.create_room_name_hint)) errorMessage(data?.nameInlineError) -// onBind { _, view, _ -> -// if (shouldForceFocusOnce && data?.name.isNullOrBlank()) { -// shouldForceFocusOnce = false -// // sad face :( -// view.textInputEditText.post { -// view.textInputEditText.showKeyboard(true) -// } -// } -// } onTextChange { text -> host.listener?.onNameChange(text) } } + if (data?.spaceType == SpaceType.Public) { + formEditTextItem { + id("alias") + enabled(true) + forceUpdateValue(!data.aliasManuallyModified) + value(data.aliasLocalPart) + hint(host.stringProvider.getString(R.string.create_space_alias_hint)) + suffixText(":" + data.homeServerName) + prefixText("#") + onFocusChange { hasFocus -> + host.aliasTextIsFocused = hasFocus + } + errorMessage( + host.roomAliasErrorFormatter.format( + (((data.aliasVerificationTask as? Fail)?.error) as? RoomAliasError)) + ) + onTextChange(host.aliasTextWatcher) + } + } + formMultiLineEditTextItem { id("topic") enabled(true) @@ -96,5 +122,6 @@ class SpaceDetailEpoxyController @Inject constructor( fun onAvatarChange() fun onNameChange(newName: String) fun onTopicChange(newTopic: String) + fun setAliasLocalPart(aliasLocalPart: String) } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 334c7bd64d..630c578069 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -39,6 +39,7 @@ import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.alias.RoomAliasFragment import kotlinx.parcelize.Parcelize import javax.inject.Inject @@ -155,6 +156,15 @@ class SpaceManageActivity : VectorBaseActivity(), ) } } + SpaceManagedSharedViewEvents.NavigateToAliasSettings -> { + args?.spaceId?.let { spaceId -> + addFragmentToBackstack( + R.id.simpleFragmentContainer, + RoomAliasFragment::class.java, + RoomProfileArgs(spaceId) + ) + } + } } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt index 57c47250f9..f1d041056f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt @@ -56,6 +56,7 @@ class SpaceManageSharedViewModel @AssistedInject constructor( SpaceManagedSharedAction.ShowLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.ShowLoading) SpaceManagedSharedAction.CreateRoom -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateRoom) SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms) + SpaceManagedSharedAction.OpenSpaceAliasesSettings -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToAliasSettings) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt index 0b413a3b8a..77143470bc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt @@ -24,4 +24,5 @@ sealed class SpaceManagedSharedAction : VectorViewModelAction { object HideLoading : SpaceManagedSharedAction() object CreateRoom : SpaceManagedSharedAction() object ManageRooms : SpaceManagedSharedAction() + object OpenSpaceAliasesSettings : SpaceManagedSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt index da6f01d205..ab993764c6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt @@ -24,4 +24,5 @@ sealed class SpaceManagedSharedViewEvents : VectorViewEvents { object HideLoading : SpaceManagedSharedViewEvents() object NavigateToCreateRoom : SpaceManagedSharedViewEvents() object NavigateToManageRooms : SpaceManagedSharedViewEvents() + object NavigateToAliasSettings : SpaceManagedSharedViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt index 08b21db025..27204be8a6 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -52,6 +52,7 @@ class SpaceSettingsController @Inject constructor( fun onDevRoomSettings() fun onManageRooms() fun setIsPublic(public: Boolean) + fun onRoomAliasesClicked() } var callback: Callback? = null @@ -103,6 +104,7 @@ class SpaceSettingsController @Inject constructor( } } + val isPublic = (data.newRoomJoinRules.newJoinRules ?: data.currentRoomJoinRules) == RoomJoinRules.PUBLIC if (vectorPreferences.labsUseExperimentalRestricted()) { buildProfileAction( id = "joinRule", @@ -113,7 +115,6 @@ class SpaceSettingsController @Inject constructor( action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } ) } else { - val isPublic = (data.newRoomJoinRules.newJoinRules ?: data.currentRoomJoinRules) == RoomJoinRules.PUBLIC formSwitchItem { id("isPublic") enabled(data.actionPermissions.canChangeJoinRule) @@ -133,13 +134,24 @@ class SpaceSettingsController @Inject constructor( id = "manage_rooms", title = stringProvider.getString(R.string.space_settings_manage_rooms), // subtitle = data.getJoinRuleWording(stringProvider), - divider = vectorPreferences.developerMode(), + divider = vectorPreferences.developerMode() || isPublic, editable = data.actionPermissions.canAddChildren, action = { if (data.actionPermissions.canAddChildren) callback?.onManageRooms() } ) + if (isPublic) { + buildProfileAction( + id = "alias", + title = stringProvider.getString(R.string.space_settings_alias_title), + subtitle = stringProvider.getString(R.string.space_settings_alias_subtitle), + divider = vectorPreferences.developerMode(), + editable = true, + action = { callback?.onRoomAliasesClicked() } + ) + } + if (vectorPreferences.developerMode()) { buildProfileAction( id = "dev_tools", diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt index 350c0bdb4a..e831732bcc 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -233,6 +233,10 @@ class SpaceSettingsFragment @Inject constructor( } } + override fun onRoomAliasesClicked() { + sharedViewModel.handle(SpaceManagedSharedAction.OpenSpaceAliasesSettings) + } + override fun onImageReady(uri: Uri?) { uri ?: return viewModel.handle( 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 deleted file mode 100644 index 1cac4763c4..0000000000 --- a/vector/src/main/res/layout/item_room_alias_text_input.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 621ecb7aee..54356db664 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1414,6 +1414,8 @@ Room addresses See and managed addresses of this room, and its visibility in the room directory. + Space addresses + See and managed addresses of this space. Room Addresses Published Addresses @@ -2585,6 +2587,7 @@ 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 + Space address This address is already in use Please provide a room address Some characters are not allowed