Merge pull request #2416 from vector-im/feature/bma_create_room_form

Improve create room form
This commit is contained in:
Benoit Marty 2020-11-20 13:57:46 +01:00 committed by GitHub
commit a87e44bae4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 553 additions and 127 deletions

View File

@ -10,6 +10,7 @@ Improvements 🙌:
- Use RoomMember instead of User in the context of a Room. - Use RoomMember instead of User in the context of a Room.
- Ask for explicit user consent to send their contact details to the identity server (#2375) - Ask for explicit user consent to send their contact details to the identity server (#2375)
- Handle events of type "m.room.server_acl" (#890) - Handle events of type "m.room.server_acl" (#890)
- Room creation form: add advanced section to disable federation (#1314)
- Move "Enable Encryption" from room setting screen to room profile screen (#2394) - Move "Enable Encryption" from room setting screen to room profile screen (#2394)
Bugfix 🐛: Bugfix 🐛:

View File

@ -22,4 +22,9 @@ import org.matrix.android.sdk.api.failure.MatrixError
sealed class CreateRoomFailure : Failure.FeatureFailure() { sealed class CreateRoomFailure : Failure.FeatureFailure() {
object CreatedWithTimeout : CreateRoomFailure() object CreatedWithTimeout : CreateRoomFailure()
data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure() data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure()
sealed class RoomAliasError : CreateRoomFailure() {
object AliasEmpty : RoomAliasError()
object AliasNotAvailable : RoomAliasError()
object AliasInvalid : RoomAliasError()
}
} }

View File

@ -94,7 +94,22 @@ class CreateRoomParams {
* The server will clobber the following keys: creator. * The server will clobber the following keys: creator.
* Future versions of the specification may allow the server to clobber other keys. * Future versions of the specification may allow the server to clobber other keys.
*/ */
var creationContent: Any? = null val creationContent = mutableMapOf<String, Any>()
/**
* Set to true to disable federation of this room.
* Default: false
*/
var disableFederation = false
set(value) {
field = value
if (value) {
creationContent[CREATION_CONTENT_KEY_M_FEDERATE] = false
} else {
// This is the default value, we remove the field
creationContent.remove(CREATION_CONTENT_KEY_M_FEDERATE)
}
}
/** /**
* The power level content to override in the default power level event * The power level content to override in the default power level event
@ -120,4 +135,8 @@ class CreateRoomParams {
fun enableEncryption() { fun enableEncryption() {
algorithm = MXCRYPTO_ALGORITHM_MEGOLM algorithm = MXCRYPTO_ALGORITHM_MEGOLM
} }
companion object {
private const val CREATION_CONTENT_KEY_M_FEDERATE = "m.federate"
}
} }

View File

@ -74,8 +74,8 @@ internal data class CreateRoomBody(
val invite3pids: List<ThreePidInviteBody>?, val invite3pids: List<ThreePidInviteBody>?,
/** /**
* Extra keys to be added to the content of the m.room.create. * Extra keys, such as m.federate, to be added to the content of the m.room.create event.
* The server will clobber the following keys: creator. * The server will clobber the following keys: creator, room_version.
* Future versions of the specification may allow the server to clobber other keys. * Future versions of the specification may allow the server to clobber other keys.
*/ */
@Json(name = "creation_content") @Json(name = "creation_content")

View File

@ -81,7 +81,7 @@ internal class CreateRoomBodyBuilder @Inject constructor(
topic = params.topic, topic = params.topic,
invitedUserIds = params.invitedUserIds, invitedUserIds = params.invitedUserIds,
invite3pids = invite3pids, invite3pids = invite3pids,
creationContent = params.creationContent, creationContent = params.creationContent.takeIf { it.isNotEmpty() },
initialStates = initialStates, initialStates = initialStates,
preset = params.preset, preset = params.preset,
isDirect = params.isDirect, isDirect = params.isDirect,

View File

@ -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.model.RoomSummaryEntity
import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.database.query.where
import org.matrix.android.sdk.internal.di.SessionDatabase 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.network.executeRequest
import org.matrix.android.sdk.internal.session.room.RoomAPI 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.room.read.SetReadMarkersTask
import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper import org.matrix.android.sdk.internal.session.user.accountdata.DirectChatsHelper
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask import org.matrix.android.sdk.internal.session.user.accountdata.UpdateUserAccountDataTask
@ -45,6 +47,7 @@ internal interface CreateRoomTask : Task<CreateRoomParams, String>
internal class DefaultCreateRoomTask @Inject constructor( internal class DefaultCreateRoomTask @Inject constructor(
private val roomAPI: RoomAPI, private val roomAPI: RoomAPI,
@UserId private val userId: String,
@SessionDatabase private val monarchy: Monarchy, @SessionDatabase private val monarchy: Monarchy,
private val directChatsHelper: DirectChatsHelper, private val directChatsHelper: DirectChatsHelper,
private val updateUserAccountDataTask: UpdateUserAccountDataTask, 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") ?: throw IllegalStateException("You can't create a direct room without an invitedUser")
} else null } else null
if (params.preset == CreateRoomPreset.PRESET_PUBLIC_CHAT) {
if (params.roomAliasName.isNullOrEmpty()) {
throw CreateRoomFailure.RoomAliasError.AliasEmpty
}
// Check alias availability
val fullAlias = "#" + params.roomAliasName + ":" + userId.substringAfter(":")
try {
executeRequest<RoomAliasDescription>(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.RoomAliasError.AliasNotAvailable
}
}
val createRoomBody = createRoomBodyBuilder.build(params) val createRoomBody = createRoomBodyBuilder.build(params)
val createRoomResponse = try { val createRoomResponse = try {
@ -68,14 +96,18 @@ internal class DefaultCreateRoomTask @Inject constructor(
apiCall = roomAPI.createRoom(createRoomBody) apiCall = roomAPI.createRoom(createRoomBody)
} }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is Failure.ServerError if (throwable is Failure.ServerError) {
&& throwable.httpCode == 403 if (throwable.httpCode == 403
&& throwable.error.code == MatrixError.M_FORBIDDEN && throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message.startsWith("Federation denied with")) { && throwable.error.message.startsWith("Federation denied with")) {
throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error) throw CreateRoomFailure.CreatedWithFederationFailure(throwable.error)
} else { } else if (throwable.httpCode == 400
throw throwable && throwable.error.code == MatrixError.M_UNKNOWN
&& throwable.error.message == "Invalid characters in room alias") {
throw CreateRoomFailure.RoomAliasError.AliasInvalid
}
} }
throw throwable
} }
val roomId = createRoomResponse.roomId 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) // Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)

View File

@ -26,12 +26,12 @@ import javax.inject.Inject
class DrawableProvider @Inject constructor(private val context: Context) { class DrawableProvider @Inject constructor(private val context: Context) {
fun getDrawable(@DrawableRes colorRes: Int): Drawable? { fun getDrawable(@DrawableRes drawableRes: Int): Drawable? {
return ContextCompat.getDrawable(context, colorRes) return ContextCompat.getDrawable(context, drawableRes)
} }
fun getDrawable(@DrawableRes colorRes: Int, @ColorInt color: Int): Drawable? { fun getDrawable(@DrawableRes drawableRes: Int, @ColorInt color: Int): Drawable? {
return ContextCompat.getDrawable(context, colorRes)?.let { return ContextCompat.getDrawable(context, drawableRes)?.let {
ThemeUtils.tintDrawableWithColor(it, color) ThemeUtils.tintDrawableWithColor(it, color)
} }
} }

View File

@ -0,0 +1,51 @@
/*
* 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.form
import android.widget.TextView
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyHolder
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.features.themes.ThemeUtils
@EpoxyModelClass(layout = R.layout.item_form_advanced_toggle)
abstract class FormAdvancedToggleItem : VectorEpoxyModel<FormAdvancedToggleItem.Holder>() {
@EpoxyAttribute lateinit var title: CharSequence
@EpoxyAttribute var expanded: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) {
super.bind(holder)
val tintColor = ThemeUtils.getColor(holder.view.context, R.attr.riotx_text_secondary)
val expandedArrowDrawableRes = if (expanded) R.drawable.ic_expand_more_white else R.drawable.ic_expand_less_white
val expandedArrowDrawable = ContextCompat.getDrawable(holder.view.context, expandedArrowDrawableRes)?.also {
DrawableCompat.setTint(it, tintColor)
}
holder.titleView.setCompoundDrawablesWithIntrinsicBounds(null, null, expandedArrowDrawable, null)
holder.titleView.text = title
holder.view.setOnClickListener { listener?.invoke() }
}
class Holder : VectorEpoxyHolder() {
val titleView by bind<TextView>(R.id.itemFormAdvancedToggleTitleView)
}
}

View File

@ -16,7 +16,9 @@
package im.vector.app.features.form package im.vector.app.features.form
import android.view.View
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.google.android.material.switchmaterial.SwitchMaterial import com.google.android.material.switchmaterial.SwitchMaterial
@ -43,6 +45,9 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
var summary: String? = null var summary: String? = null
@EpoxyAttribute
var showDivider: Boolean = true
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.view.setOnClickListener { holder.view.setOnClickListener {
@ -61,6 +66,7 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
holder.switchView.setOnCheckedChangeListener { _, isChecked -> holder.switchView.setOnCheckedChangeListener { _, isChecked ->
listener?.invoke(isChecked) listener?.invoke(isChecked)
} }
holder.divider.isVisible = showDivider
} }
override fun shouldSaveViewState(): Boolean { override fun shouldSaveViewState(): Boolean {
@ -77,5 +83,6 @@ abstract class FormSwitchItem : VectorEpoxyModel<FormSwitchItem.Holder>() {
val titleView by bind<TextView>(R.id.formSwitchTitle) val titleView by bind<TextView>(R.id.formSwitchTitle)
val summaryView by bind<TextView>(R.id.formSwitchSummary) val summaryView by bind<TextView>(R.id.formSwitchSummary)
val switchView by bind<SwitchMaterial>(R.id.formSwitchSwitch) val switchView by bind<SwitchMaterial>(R.id.formSwitchSwitch)
val divider by bind<View>(R.id.formSwitchDivider)
} }
} }

View File

@ -26,18 +26,15 @@ import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
import im.vector.app.core.extensions.addFragmentToBackstack import im.vector.app.core.extensions.addFragmentToBackstack
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.features.roomdirectory.createroom.CreateRoomAction
import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment
import im.vector.app.features.roomdirectory.createroom.CreateRoomViewModel import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs
import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment import im.vector.app.features.roomdirectory.picker.RoomDirectoryPickerFragment
import javax.inject.Inject import javax.inject.Inject
class RoomDirectoryActivity : VectorBaseActivity() { class RoomDirectoryActivity : VectorBaseActivity() {
@Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory
@Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory @Inject lateinit var roomDirectoryViewModelFactory: RoomDirectoryViewModel.Factory
private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel() private val roomDirectoryViewModel: RoomDirectoryViewModel by viewModel()
private val createRoomViewModel: CreateRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel
override fun getLayoutRes() = R.layout.activity_simple override fun getLayoutRes() = R.layout.activity_simple
@ -60,10 +57,13 @@ class RoomDirectoryActivity : VectorBaseActivity() {
when (sharedAction) { when (sharedAction) {
is RoomDirectorySharedAction.Back -> onBackPressed() is RoomDirectorySharedAction.Back -> onBackPressed()
is RoomDirectorySharedAction.CreateRoom -> { is RoomDirectorySharedAction.CreateRoom -> {
addFragmentToBackstack(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) // Transmit the filter to the CreateRoomFragment
// Transmit the filter to the createRoomViewModel
withState(roomDirectoryViewModel) { withState(roomDirectoryViewModel) {
createRoomViewModel.handle(CreateRoomAction.SetName(it.currentFilter)) addFragmentToBackstack(
R.id.simpleFragmentContainer,
CreateRoomFragment::class.java,
CreateRoomArgs(it.currentFilter)
)
} }
} }
is RoomDirectorySharedAction.ChangeProtocol -> is RoomDirectorySharedAction.ChangeProtocol ->

View File

@ -24,9 +24,12 @@ sealed class CreateRoomAction : VectorViewModelAction {
data class SetName(val name: String) : CreateRoomAction() data class SetName(val name: String) : CreateRoomAction()
data class SetTopic(val topic: String) : CreateRoomAction() data class SetTopic(val topic: String) : CreateRoomAction()
data class SetIsPublic(val isPublic: Boolean) : 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() data class SetIsEncrypted(val isEncrypted: Boolean) : CreateRoomAction()
object ToggleShowAdvanced : CreateRoomAction()
data class DisableFederation(val disableFederation: Boolean) : CreateRoomAction()
object Create : CreateRoomAction() object Create : CreateRoomAction()
object Reset : CreateRoomAction() object Reset : CreateRoomAction()
} }

View File

@ -20,7 +20,6 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import com.airbnb.mvrx.viewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.di.ScreenComponent import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.addFragment import im.vector.app.core.extensions.addFragment
@ -28,16 +27,12 @@ import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import javax.inject.Inject
/** /**
* Simple container for [CreateRoomFragment] * Simple container for [CreateRoomFragment]
*/ */
class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable { class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var createRoomViewModelFactory: CreateRoomViewModel.Factory
private val createRoomViewModel: CreateRoomViewModel by viewModel()
private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel
override fun getLayoutRes() = R.layout.activity_simple override fun getLayoutRes() = R.layout.activity_simple
@ -52,8 +47,11 @@ class CreateRoomActivity : VectorBaseActivity(), ToolbarConfigurable {
override fun initUiAndData() { override fun initUiAndData() {
if (isFirstCreation()) { if (isFirstCreation()) {
addFragment(R.id.simpleFragmentContainer, CreateRoomFragment::class.java) addFragment(
createRoomViewModel.handle(CreateRoomAction.SetName(intent?.getStringExtra(INITIAL_NAME) ?: "")) R.id.simpleFragmentContainer,
CreateRoomFragment::class.java,
CreateRoomArgs(intent?.getStringExtra(INITIAL_NAME) ?: "")
)
} }
} }

View File

@ -19,18 +19,16 @@ package im.vector.app.features.roomdirectory.createroom
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.discovery.settingsSectionTitleItem import im.vector.app.features.discovery.settingsSectionTitleItem
import im.vector.app.features.form.formAdvancedToggleItem
import im.vector.app.features.form.formEditTextItem import im.vector.app.features.form.formEditTextItem
import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formEditableAvatarItem
import im.vector.app.features.form.formSubmitButtonItem import im.vector.app.features.form.formSubmitButtonItem
import im.vector.app.features.form.formSwitchItem import im.vector.app.features.form.formSwitchItem
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import javax.inject.Inject import javax.inject.Inject
class CreateRoomController @Inject constructor(private val stringProvider: StringProvider, class CreateRoomController @Inject constructor(private val stringProvider: StringProvider,
@ -42,31 +40,8 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
var index = 0 var index = 0
override fun buildModels(viewState: CreateRoomViewState) { override fun buildModels(viewState: CreateRoomViewState) {
when (val asyncCreateRoom = viewState.asyncCreateRoomRequest) { // display the form
is Success -> { buildForm(viewState, viewState.asyncCreateRoomRequest !is Loading)
// Nothing to display, the screen will be closed
}
is Loading -> {
// display the form
buildForm(viewState, false)
loadingItem {
id("loading")
}
}
is Uninitialized -> {
// display the form
buildForm(viewState, true)
}
is Fail -> {
// display the form
buildForm(viewState, true)
errorWithRetryItem {
id("error")
text(errorFormatter.toHumanReadable(asyncCreateRoom.error))
listener { listener?.retry() }
}
}
}
} }
private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) { private fun buildForm(viewState: CreateRoomViewState, enableFormElement: Boolean) {
@ -114,38 +89,68 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
enabled(enableFormElement) enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_public_title)) title(stringProvider.getString(R.string.create_room_public_title))
summary(stringProvider.getString(R.string.create_room_public_description)) 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 { value ->
listener?.setIsPublic(value) listener?.setIsPublic(value)
} }
} }
formSwitchItem { if (viewState.roomType is CreateRoomViewState.RoomType.Public) {
id("directory") // Room alias for public room
enabled(enableFormElement) roomAliasEditItem {
title(stringProvider.getString(R.string.create_room_directory_title)) id("alias")
summary(stringProvider.getString(R.string.create_room_directory_description)) enabled(enableFormElement)
switchChecked(viewState.isInRoomDirectory) value(viewState.roomType.aliasLocalPart)
homeServer(":" + viewState.homeServerName)
errorMessage(
when ((viewState.asyncCreateRoomRequest as? Fail)?.error) {
is CreateRoomFailure.RoomAliasError.AliasEmpty -> R.string.create_room_alias_empty
is CreateRoomFailure.RoomAliasError.AliasNotAvailable -> R.string.create_room_alias_already_in_use
is CreateRoomFailure.RoomAliasError.AliasInvalid -> R.string.create_room_alias_invalid
else -> null
}
?.let { stringProvider.getString(it) }
)
onTextChange { value ->
listener?.setAliasLocalPart(value)
}
}
} else {
// Room encryption for private room
formSwitchItem {
id("encryption")
enabled(enableFormElement)
title(stringProvider.getString(R.string.create_room_encryption_title))
summary(
if (viewState.hsAdminHasDisabledE2E) {
stringProvider.getString(R.string.settings_hs_admin_e2e_disabled)
} else {
stringProvider.getString(R.string.create_room_encryption_description)
}
)
switchChecked(viewState.isEncrypted)
listener { value -> listener { value ->
listener?.setIsInRoomDirectory(value) listener?.setIsEncrypted(value)
}
} }
} }
formSwitchItem { formAdvancedToggleItem {
id("encryption") id("showAdvanced")
enabled(enableFormElement) title(stringProvider.getString(if (viewState.showAdvanced) R.string.hide_advanced else R.string.show_advanced))
title(stringProvider.getString(R.string.create_room_encryption_title)) expanded(!viewState.showAdvanced)
summary( listener { listener?.toggleShowAdvanced() }
if (viewState.hsAdminHasDisabledE2E) { }
stringProvider.getString(R.string.settings_hs_admin_e2e_disabled) if (viewState.showAdvanced) {
} else { formSwitchItem {
stringProvider.getString(R.string.create_room_encryption_description) id("federation")
} enabled(enableFormElement)
) title(stringProvider.getString(R.string.create_room_disable_federation_title, viewState.homeServerName))
switchChecked(viewState.isEncrypted) summary(stringProvider.getString(R.string.create_room_disable_federation_description))
switchChecked(viewState.disableFederation)
listener { value -> showDivider(false)
listener?.setIsEncrypted(value) listener { value -> listener?.setDisableFederation(value) }
} }
} }
formSubmitButtonItem { formSubmitButtonItem {
@ -162,9 +167,10 @@ class CreateRoomController @Inject constructor(private val stringProvider: Strin
fun onNameChange(newName: String) fun onNameChange(newName: String)
fun onTopicChange(newTopic: String) fun onTopicChange(newTopic: String)
fun setIsPublic(isPublic: Boolean) fun setIsPublic(isPublic: Boolean)
fun setIsInRoomDirectory(isInRoomDirectory: Boolean) fun setAliasLocalPart(aliasLocalPart: String)
fun setIsEncrypted(isEncrypted: Boolean) fun setIsEncrypted(isEncrypted: Boolean)
fun retry() fun toggleShowAdvanced()
fun setDisableFederation(disableFederation: Boolean)
fun submit() fun submit()
} }
} }

View File

@ -18,10 +18,14 @@ package im.vector.app.features.roomdirectory.createroom
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
@ -33,12 +37,20 @@ import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedAction
import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_create_room.* import kotlinx.android.synthetic.main.fragment_create_room.*
import timber.log.Timber import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import javax.inject.Inject import javax.inject.Inject
@Parcelize
data class CreateRoomArgs(
val initialName: String
) : Parcelable
class CreateRoomFragment @Inject constructor( class CreateRoomFragment @Inject constructor(
private val createRoomController: CreateRoomController, private val createRoomController: CreateRoomController,
val createRoomViewModelFactory: CreateRoomViewModel.Factory,
colorProvider: ColorProvider colorProvider: ColorProvider
) : VectorBaseFragment(), ) : VectorBaseFragment(),
CreateRoomController.Listener, CreateRoomController.Listener,
@ -46,7 +58,8 @@ class CreateRoomFragment @Inject constructor(
OnBackPressed { OnBackPressed {
private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel private lateinit var sharedActionViewModel: RoomDirectorySharedActionViewModel
private val viewModel: CreateRoomViewModel by activityViewModel() private val viewModel: CreateRoomViewModel by fragmentViewModel()
private val args: CreateRoomArgs by args()
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
@ -56,17 +69,31 @@ class CreateRoomFragment @Inject constructor(
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
vectorBaseActivity.setSupportActionBar(createRoomToolbar) vectorBaseActivity.setSupportActionBar(createRoomToolbar)
sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java) sharedActionViewModel = activityViewModelProvider.get(RoomDirectorySharedActionViewModel::class.java)
setupWaitingView()
setupRecyclerView() setupRecyclerView()
createRoomClose.debouncedClicks { createRoomClose.debouncedClicks {
sharedActionViewModel.post(RoomDirectorySharedAction.Back) sharedActionViewModel.post(RoomDirectorySharedAction.Back)
} }
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed() CreateRoomViewEvents.Quit -> vectorBaseActivity.onBackPressed()
is CreateRoomViewEvents.Failure -> showFailure(it.throwable)
}.exhaustive }.exhaustive
} }
} }
override fun showFailure(throwable: Throwable) {
// Note: RoomAliasError are displayed directly in the form
if (throwable !is CreateRoomFailure.RoomAliasError) {
super.showFailure(throwable)
}
}
private fun setupWaitingView() {
waiting_view_status_text.isVisible = true
waiting_view_status_text.setText(R.string.create_room_in_progress)
}
override fun onDestroyView() { override fun onDestroyView() {
createRoomForm.cleanup() createRoomForm.cleanup()
createRoomController.listener = null createRoomController.listener = null
@ -102,20 +129,23 @@ class CreateRoomFragment @Inject constructor(
viewModel.handle(CreateRoomAction.SetIsPublic(isPublic)) viewModel.handle(CreateRoomAction.SetIsPublic(isPublic))
} }
override fun setIsInRoomDirectory(isInRoomDirectory: Boolean) { override fun setAliasLocalPart(aliasLocalPart: String) {
viewModel.handle(CreateRoomAction.SetIsInRoomDirectory(isInRoomDirectory)) viewModel.handle(CreateRoomAction.SetRoomAliasLocalPart(aliasLocalPart))
} }
override fun setIsEncrypted(isEncrypted: Boolean) { override fun setIsEncrypted(isEncrypted: Boolean) {
viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted)) viewModel.handle(CreateRoomAction.SetIsEncrypted(isEncrypted))
} }
override fun submit() { override fun toggleShowAdvanced() {
viewModel.handle(CreateRoomAction.Create) viewModel.handle(CreateRoomAction.ToggleShowAdvanced)
} }
override fun retry() { override fun setDisableFederation(disableFederation: Boolean) {
Timber.v("Retry") viewModel.handle(CreateRoomAction.DisableFederation(disableFederation))
}
override fun submit() {
viewModel.handle(CreateRoomAction.Create) viewModel.handle(CreateRoomAction.Create)
} }
@ -139,6 +169,7 @@ class CreateRoomFragment @Inject constructor(
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
val async = state.asyncCreateRoomRequest val async = state.asyncCreateRoomRequest
waiting_view.isVisible = async is Loading
if (async is Success) { if (async is Success) {
// Navigate to freshly created room // Navigate to freshly created room
navigator.openRoom(requireActivity(), async()) navigator.openRoom(requireActivity(), async())

View File

@ -22,5 +22,6 @@ import im.vector.app.core.platform.VectorViewEvents
* Transient events for room creation screen * Transient events for room creation screen
*/ */
sealed class CreateRoomViewEvents : VectorViewEvents { sealed class CreateRoomViewEvents : VectorViewEvents {
data class Failure(val throwable: Throwable) : CreateRoomViewEvents()
object Quit : CreateRoomViewEvents() object Quit : CreateRoomViewEvents()
} }

View File

@ -17,13 +17,13 @@
package im.vector.app.features.roomdirectory.createroom package im.vector.app.features.roomdirectory.createroom
import androidx.core.net.toFile import androidx.core.net.toFile
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
@ -31,7 +31,6 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.raw.wellknown.getElementWellknown import im.vector.app.features.raw.wellknown.getElementWellknown
import im.vector.app.features.raw.wellknown.isE2EByDefault import im.vector.app.features.raw.wellknown.isE2EByDefault
import im.vector.app.features.roomdirectory.RoomDirectoryActivity
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
@ -53,9 +52,18 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
} }
init { init {
initHomeServerName()
initAdminE2eByDefault() initAdminE2eByDefault()
} }
private fun initHomeServerName() {
setState {
copy(
homeServerName = session.myUserId.substringAfter(":")
)
}
}
private var adminE2EByDefault = true private var adminE2EByDefault = true
private fun initAdminE2eByDefault() { private fun initAdminE2eByDefault() {
@ -68,7 +76,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
setState { setState {
copy( copy(
isEncrypted = !isPublic && adminE2EByDefault, isEncrypted = roomType is CreateRoomViewState.RoomType.Private && adminE2EByDefault,
hsAdminHasDisabledE2E = !adminE2EByDefault hsAdminHasDisabledE2E = !adminE2EByDefault
) )
} }
@ -79,29 +87,43 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
@JvmStatic @JvmStatic
override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? { override fun create(viewModelContext: ViewModelContext, state: CreateRoomViewState): CreateRoomViewModel? {
val activity: FragmentActivity = (viewModelContext as ActivityViewModelContext).activity() val fragment: CreateRoomFragment = (viewModelContext as FragmentViewModelContext).fragment()
return when (activity) { return fragment.createRoomViewModelFactory.create(state)
is CreateRoomActivity -> activity.createRoomViewModelFactory.create(state)
is RoomDirectoryActivity -> activity.createRoomViewModelFactory.create(state)
else -> error("Wrong activity")
}
} }
} }
override fun handle(action: CreateRoomAction) { override fun handle(action: CreateRoomAction) {
when (action) { when (action) {
is CreateRoomAction.SetAvatar -> setAvatar(action) is CreateRoomAction.SetAvatar -> setAvatar(action)
is CreateRoomAction.SetName -> setName(action) is CreateRoomAction.SetName -> setName(action)
is CreateRoomAction.SetTopic -> setTopic(action) is CreateRoomAction.SetTopic -> setTopic(action)
is CreateRoomAction.SetIsPublic -> setIsPublic(action) is CreateRoomAction.SetIsPublic -> setIsPublic(action)
is CreateRoomAction.SetIsInRoomDirectory -> setIsInRoomDirectory(action) is CreateRoomAction.SetRoomAliasLocalPart -> setRoomAliasLocalPart(action)
is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action) is CreateRoomAction.SetIsEncrypted -> setIsEncrypted(action)
is CreateRoomAction.Create -> doCreateRoom() is CreateRoomAction.Create -> doCreateRoom()
CreateRoomAction.Reset -> doReset() CreateRoomAction.Reset -> doReset()
CreateRoomAction.ToggleShowAdvanced -> toggleShowAdvanced()
is CreateRoomAction.DisableFederation -> disableFederation(action)
}.exhaustive }.exhaustive
} }
private fun disableFederation(action: CreateRoomAction.DisableFederation) {
setState {
copy(disableFederation = action.disableFederation)
}
}
private fun toggleShowAdvanced() {
setState {
copy(
showAdvanced = !showAdvanced,
// Reset to false if advanced is hidden
disableFederation = disableFederation && !showAdvanced
)
}
}
private fun doReset() { private fun doReset() {
setState { setState {
// Delete temporary file with the avatar // Delete temporary file with the avatar
@ -123,13 +145,35 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) } private fun setTopic(action: CreateRoomAction.SetTopic) = setState { copy(roomTopic = action.topic) }
private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState { private fun setIsPublic(action: CreateRoomAction.SetIsPublic) = setState {
copy( if (action.isPublic) {
isPublic = action.isPublic, copy(
isEncrypted = !action.isPublic && adminE2EByDefault roomType = CreateRoomViewState.RoomType.Public(""),
) // Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized,
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),
// Reset any error in the form about alias
asyncCreateRoomRequest = Uninitialized
)
}
}
}
// Else ignore
}
private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) } private fun setIsEncrypted(action: CreateRoomAction.SetIsEncrypted) = setState { copy(isEncrypted = action.isEncrypted) }
@ -147,10 +191,23 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
name = state.roomName.takeIf { it.isNotBlank() } name = state.roomName.takeIf { it.isNotBlank() }
topic = state.roomTopic.takeIf { it.isNotBlank() } topic = state.roomTopic.takeIf { it.isNotBlank() }
avatarUri = state.avatarUri avatarUri = state.avatarUri
// Directory visibility when (state.roomType) {
visibility = if (state.isInRoomDirectory) RoomDirectoryVisibility.PUBLIC else RoomDirectoryVisibility.PRIVATE is CreateRoomViewState.RoomType.Public -> {
// Public room // Directory visibility
preset = if (state.isPublic) CreateRoomPreset.PRESET_PUBLIC_CHAT else CreateRoomPreset.PRESET_PRIVATE_CHAT 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
// Encryption // Encryption
if (state.isEncrypted) { if (state.isEncrypted) {
@ -169,6 +226,7 @@ class CreateRoomViewModel @AssistedInject constructor(@Assisted initialState: Cr
setState { setState {
copy(asyncCreateRoomRequest = Fail(failure)) copy(asyncCreateRoomRequest = Fail(failure))
} }
_viewEvents.post(CreateRoomViewEvents.Failure(failure))
} }
}) })
} }

View File

@ -20,20 +20,35 @@ import android.net.Uri
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.api.extensions.orTrue
data class CreateRoomViewState( data class CreateRoomViewState(
val avatarUri: Uri? = null, val avatarUri: Uri? = null,
val roomName: String = "", val roomName: String = "",
val roomTopic: String = "", val roomTopic: String = "",
val isPublic: Boolean = false, val roomType: RoomType = RoomType.Private,
val isInRoomDirectory: Boolean = false,
val isEncrypted: Boolean = false, val isEncrypted: Boolean = false,
val showAdvanced: Boolean = false,
val disableFederation: Boolean = false,
val homeServerName: String = "",
val hsAdminHasDisabledE2E: Boolean = false, val hsAdminHasDisabledE2E: Boolean = false,
val asyncCreateRoomRequest: Async<String> = Uninitialized val asyncCreateRoomRequest: Async<String> = Uninitialized
) : MvRxState { ) : MvRxState {
constructor(args: CreateRoomArgs) : this(
roomName = args.initialName
)
/** /**
* Return true if there is not important input from user * Return true if there is not important input from user
*/ */
fun isEmpty() = avatarUri == null && roomName.isEmpty() && roomTopic.isEmpty() fun isEmpty() = avatarUri == null
&& roomName.isEmpty()
&& roomTopic.isEmpty()
&& (roomType as? RoomType.Public)?.aliasLocalPart?.isEmpty().orTrue()
sealed class RoomType {
object Private : RoomType()
data class Public(val aliasLocalPart: String) : RoomType()
}
} }

View File

@ -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<RoomAliasEditItem.Holder>() {
@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<TextInputLayout>(R.id.itemRoomAliasTextInputLayout)
val textInputEditText by bind<TextInputEditText>(R.id.itemRoomAliasTextInputEditText)
val homeServerText by bind<TextView>(R.id.itemRoomAliasHomeServer)
val bottomSeparator by bind<View>(R.id.itemRoomAliasDivider)
}
}

View File

@ -67,5 +67,7 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<include layout="@layout/merge_overlay_waiting_view" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemFormAdvancedToggleRootView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/layout_horizontal_margin"
android:paddingTop="12dp"
android:paddingEnd="@dimen/layout_horizontal_margin"
android:paddingBottom="12dp">
<TextView
android:id="@+id/itemFormAdvancedToggleTitleView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="?riotx_text_secondary"
android:textSize="14sp"
android:textStyle="bold"
app:drawableTint="?riotx_text_secondary"
tools:drawableEnd="@drawable/ic_expand_more_white"
tools:text="@string/show_advanced" />
</LinearLayout>

View File

@ -15,8 +15,6 @@
android:layout_marginStart="@dimen/layout_horizontal_margin" android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginEnd="@dimen/layout_horizontal_margin" android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:duplicateParentState="true" android:duplicateParentState="true"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="15sp" android:textSize="15sp"
app:layout_constraintBottom_toTopOf="@+id/formSwitchSummary" app:layout_constraintBottom_toTopOf="@+id/formSwitchSummary"

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?riotx_background"
android:minHeight="@dimen/item_form_min_height">
<TextView
android:id="@+id/itemRoomAliasHash"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:text="@string/matrix_room_alias_prefix"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="@+id/itemRoomAliasTextInputLayout"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/itemRoomAliasTextInputLayout"
style="@style/VectorTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:errorEnabled="true"
app:layout_constraintBottom_toTopOf="@+id/itemRoomAliasDivider"
app:layout_constraintEnd_toStartOf="@+id/itemRoomAliasHomeServer"
app:layout_constraintStart_toEndOf="@+id/itemRoomAliasHash"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/itemRoomAliasTextInputEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/create_room_alias_hint"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:id="@+id/itemRoomAliasHomeServer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:maxWidth="200dp"
android:textSize="18sp"
app:layout_constraintBaseline_toBaselineOf="@+id/itemRoomAliasTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
tools:text=":matrix.org" />
<View
android:id="@+id/itemRoomAliasDivider"
android:layout_width="0dp"
android:layout_height="1dp"
android:background="?riotx_header_panel_border_mobile"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -9,6 +9,8 @@
<string name="password_hint" translatable="false">********</string> <string name="password_hint" translatable="false">********</string>
<string name="matrix_room_alias_prefix" translatable="false">#</string>
<!-- Temporary string --> <!-- Temporary string -->
<string name="not_implemented" translatable="false">Not implemented yet in Element</string> <string name="not_implemented" translatable="false">Not implemented yet in Element</string>

View File

@ -2096,6 +2096,18 @@
<string name="create_room_encryption_title">"Enable encryption"</string> <string name="create_room_encryption_title">"Enable encryption"</string>
<string name="create_room_encryption_description">"Once enabled, encryption cannot be disabled."</string> <string name="create_room_encryption_description">"Once enabled, encryption cannot be disabled."</string>
<string name="show_advanced">Show advanced</string>
<string name="hide_advanced">Hide advanced</string>
<string name="create_room_disable_federation_title">Block anyone not part of %s from ever joining this room</string>
<string name="create_room_disable_federation_description">You might enable this if the room will only be used for collaborating with internal teams on your homeserver. This cannot be changed later.</string>
<string name="create_room_alias_hint">Room address</string>
<string name="create_room_alias_already_in_use">This address is already in use</string>
<string name="create_room_alias_empty">Please provide a room address</string>
<string name="create_room_alias_invalid">Some characters are not allowed</string>
<string name="create_room_in_progress">Creating room…</string>
<string name="login_error_threepid_denied">Your email domain is not authorized to register on this server</string> <string name="login_error_threepid_denied">Your email domain is not authorized to register on this server</string>
<string name="verification_conclusion_warning">Untrusted sign in</string> <string name="verification_conclusion_warning">Untrusted sign in</string>