Merge pull request #2416 from vector-im/feature/bma_create_room_form
Improve create room form
This commit is contained in:
commit
a87e44bae4
|
@ -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 🐛:
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) ?: "")
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue