Merge pull request #3976 from vector-im/feature/bca/space_team_invite_mail

Add invite by email screen in create space flow
This commit is contained in:
Benoit Marty 2021-09-14 20:18:35 +02:00 committed by GitHub
commit 3575157f1c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 362 additions and 137 deletions

1
changelog.d/3678.feature Normal file
View file

@ -0,0 +1 @@
Spaces | M3.23 Invite by email in create private space flow

1
changelog.d/3945.bugfix Normal file
View file

@ -0,0 +1 @@
Remove the "Teammate spaces aren't quite ready" bottom sheet

View file

@ -142,6 +142,7 @@ import im.vector.app.features.signout.soft.SoftLogoutFragment
import im.vector.app.features.spaces.SpaceListFragment import im.vector.app.features.spaces.SpaceListFragment
import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment
import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment
import im.vector.app.features.spaces.create.CreateSpaceAdd3pidInvitesFragment
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
@ -793,6 +794,11 @@ interface FragmentModule {
@FragmentKey(ChoosePrivateSpaceTypeFragment::class) @FragmentKey(ChoosePrivateSpaceTypeFragment::class)
fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment fun bindChoosePrivateSpaceTypeFragment(fragment: ChoosePrivateSpaceTypeFragment): Fragment
@Binds
@IntoMap
@FragmentKey(CreateSpaceAdd3pidInvitesFragment::class)
fun bindCreateSpaceAdd3pidInvitesFragment(fragment: CreateSpaceAdd3pidInvitesFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(SpaceAddRoomFragment::class) @FragmentKey(SpaceAddRoomFragment::class)

View file

@ -31,6 +31,7 @@ import im.vector.app.core.platform.SimpleFragmentActivity
import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment import im.vector.app.features.spaces.create.ChoosePrivateSpaceTypeFragment
import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment
import im.vector.app.features.spaces.create.CreateSpaceAction import im.vector.app.features.spaces.create.CreateSpaceAction
import im.vector.app.features.spaces.create.CreateSpaceAdd3pidInvitesFragment
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.create.CreateSpaceEvents import im.vector.app.features.spaces.create.CreateSpaceEvents
@ -55,18 +56,21 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (isFirstCreation()) { if (isFirstCreation()) {
when (withState(viewModel) { it.step }) { when (withState(viewModel) { it.step }) {
CreateSpaceState.Step.ChooseType -> { CreateSpaceState.Step.ChooseType -> {
navigateToFragment(ChooseSpaceTypeFragment::class.java) navigateToFragment(ChooseSpaceTypeFragment::class.java)
} }
CreateSpaceState.Step.SetDetails -> { CreateSpaceState.Step.SetDetails -> {
navigateToFragment(ChooseSpaceTypeFragment::class.java) navigateToFragment(ChooseSpaceTypeFragment::class.java)
} }
CreateSpaceState.Step.AddRooms -> { CreateSpaceState.Step.AddRooms -> {
navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java)
} }
CreateSpaceState.Step.ChoosePrivateType -> { CreateSpaceState.Step.ChoosePrivateType -> {
navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java) navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java)
} }
CreateSpaceState.Step.AddEmailsOrInvites -> {
navigateToFragment(CreateSpaceAdd3pidInvitesFragment::class.java)
}
} }
} }
} }
@ -92,6 +96,9 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
CreateSpaceEvents.NavigateToAddRooms -> { CreateSpaceEvents.NavigateToAddRooms -> {
navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java)
} }
CreateSpaceEvents.NavigateToAdd3Pid -> {
navigateToFragment(CreateSpaceAdd3pidInvitesFragment::class.java)
}
CreateSpaceEvents.NavigateToChoosePrivateType -> { CreateSpaceEvents.NavigateToChoosePrivateType -> {
navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java) navigateToFragment(ChoosePrivateSpaceTypeFragment::class.java)
} }
@ -143,6 +150,7 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
if (state.spaceType == SpaceType.Public) R.string.your_public_space if (state.spaceType == SpaceType.Public) R.string.your_public_space
else R.string.your_private_space else R.string.your_private_space
} }
CreateSpaceState.Step.AddEmailsOrInvites,
CreateSpaceState.Step.ChoosePrivateType -> R.string.your_private_space CreateSpaceState.Step.ChoosePrivateType -> R.string.your_private_space
} }
supportActionBar?.let { supportActionBar?.let {

View file

@ -1,46 +0,0 @@
/*
* Copyright (c) 2021 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.spaces.create
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.setFragmentResult
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetSpaceCreatePrivateWarningBinding
class BetaWarningBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpaceCreatePrivateWarningBinding>() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
BottomSheetSpaceCreatePrivateWarningBinding.inflate(inflater, container, false)
override val showExpanded = true
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.continueButton.debouncedClicks {
setFragmentResult(REQUEST_KEY, Bundle.EMPTY)
dismiss()
}
}
companion object {
const val REQUEST_KEY = "BetaWarningBottomSheet"
}
}

View file

@ -20,7 +20,6 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.setFragmentResultListener
import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.activityViewModel
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
@ -39,13 +38,6 @@ class ChoosePrivateSpaceTypeFragment @Inject constructor(
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceCreateChoosePrivateModelBinding.inflate(layoutInflater, container, false) FragmentSpaceCreateChoosePrivateModelBinding.inflate(layoutInflater, container, false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setFragmentResultListener(BetaWarningBottomSheet.REQUEST_KEY) { _, _ ->
sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.MeAndTeammates))
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@ -54,7 +46,7 @@ class ChoosePrivateSpaceTypeFragment @Inject constructor(
} }
views.teammatesButton.onClick { views.teammatesButton.onClick {
BetaWarningBottomSheet().show(parentFragmentManager, "warning") sharedViewModel.handle(CreateSpaceAction.SetSpaceTopology(SpaceTopology.MeAndTeammates))
} }
sharedViewModel.subscribe { state -> sharedViewModel.subscribe { state ->

View file

@ -28,6 +28,8 @@ sealed class CreateSpaceAction : VectorViewModelAction {
object OnBackPressed : CreateSpaceAction() object OnBackPressed : CreateSpaceAction()
object NextFromDetails : CreateSpaceAction() object NextFromDetails : CreateSpaceAction()
object NextFromDefaultRooms : CreateSpaceAction() object NextFromDefaultRooms : CreateSpaceAction()
object NextFromAdd3pid : CreateSpaceAction()
data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction() data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction()
data class DefaultInvite3pidChanged(val index: Int, val email: String) : CreateSpaceAction()
data class SetSpaceTopology(val topology: SpaceTopology) : CreateSpaceAction() data class SetSpaceTopology(val topology: SpaceTopology) : CreateSpaceAction()
} }

View file

@ -0,0 +1,93 @@
/*
* Copyright (c) 2021 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.spaces.create
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import im.vector.app.R
import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.platform.OnBackPressed
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding
import im.vector.app.features.settings.VectorSettingsActivity
import javax.inject.Inject
class CreateSpaceAdd3pidInvitesFragment @Inject constructor(
private val epoxyController: SpaceAdd3pidEpoxyController
) : VectorBaseFragment<FragmentSpaceCreateGenericEpoxyFormBinding>(),
SpaceAdd3pidEpoxyController.Listener,
OnBackPressed {
private val sharedViewModel: CreateSpaceViewModel by activityViewModel()
override fun onBackPressed(toolbarButton: Boolean): Boolean {
sharedViewModel.handle(CreateSpaceAction.OnBackPressed)
return true
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
views.recyclerView.configureWith(epoxyController)
epoxyController.listener = this
sharedViewModel.subscribe(this) {
invalidateState(it)
}
views.nextButton.setText(R.string.next_pf)
views.nextButton.debouncedClicks {
view.hideKeyboard()
sharedViewModel.handle(CreateSpaceAction.NextFromAdd3pid)
}
}
private fun invalidateState(it: CreateSpaceState) {
epoxyController.setData(it)
val noEmails = it.default3pidInvite?.all { it.value.isNullOrBlank() } ?: true
views.nextButton.text = if (noEmails) {
getString(R.string.skip_for_now)
} else {
getString(R.string.next_pf)
}
}
override fun onDestroyView() {
views.recyclerView.cleanup()
epoxyController.listener = null
super.onDestroyView()
}
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false)
override fun on3pidChange(index: Int, newName: String) {
sharedViewModel.handle(CreateSpaceAction.DefaultInvite3pidChanged(index, newName))
}
override fun onNoIdentityServer() {
navigator.openSettings(
requireContext(),
VectorSettingsActivity.EXTRA_DIRECT_ACCESS_DISCOVERY_SETTINGS
)
}
}

View file

@ -22,6 +22,7 @@ sealed class CreateSpaceEvents : VectorViewEvents {
object NavigateToDetails : CreateSpaceEvents() object NavigateToDetails : CreateSpaceEvents()
object NavigateToChooseType : CreateSpaceEvents() object NavigateToChooseType : CreateSpaceEvents()
object NavigateToAddRooms : CreateSpaceEvents() object NavigateToAddRooms : CreateSpaceEvents()
object NavigateToAdd3Pid : CreateSpaceEvents()
object NavigateToChoosePrivateType : CreateSpaceEvents() object NavigateToChoosePrivateType : CreateSpaceEvents()
object Dismiss : CreateSpaceEvents() object Dismiss : CreateSpaceEvents()
data class FinishSuccess(val spaceId: String, val defaultRoomId: String?, val topology: SpaceTopology?) : CreateSpaceEvents() data class FinishSuccess(val spaceId: String, val defaultRoomId: String?, val topology: SpaceTopology?) : CreateSpaceEvents()

View file

@ -34,13 +34,17 @@ data class CreateSpaceState(
val aliasVerificationTask: Async<Boolean> = Uninitialized, val aliasVerificationTask: Async<Boolean> = Uninitialized,
val nameInlineError: String? = null, val nameInlineError: String? = null,
val defaultRooms: Map<Int /** position in form */, String?>? = null, val defaultRooms: Map<Int /** position in form */, String?>? = null,
val creationResult: Async<String> = Uninitialized val default3pidInvite: Map<Int /** position in form */, String?>? = null,
val emailValidationResult: Map<Int /** position in form */, Boolean>? = null,
val creationResult: Async<String> = Uninitialized,
val canInviteByMail: Boolean = false
) : MvRxState { ) : MvRxState {
enum class Step { enum class Step {
ChooseType, ChooseType,
SetDetails, SetDetails,
AddRooms, AddRooms,
ChoosePrivateType ChoosePrivateType,
AddEmailsOrInvites
} }
} }

View file

@ -31,6 +31,7 @@ import dagger.assisted.AssistedInject
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.isEmail
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -38,6 +39,7 @@ import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.MatrixPatterns.getDomain import org.matrix.android.sdk.api.MatrixPatterns.getDomain
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.identity.IdentityServiceListener
import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult import org.matrix.android.sdk.api.session.room.AliasAvailabilityResult
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
@ -49,12 +51,28 @@ class CreateSpaceViewModel @AssistedInject constructor(
private val errorFormatter: ErrorFormatter private val errorFormatter: ErrorFormatter
) : VectorViewModel<CreateSpaceState, CreateSpaceAction, CreateSpaceEvents>(initialState) { ) : VectorViewModel<CreateSpaceState, CreateSpaceAction, CreateSpaceEvents>(initialState) {
private val identityService = session.identityService()
private val identityServerManagerListener = object : IdentityServiceListener {
override fun onIdentityServerChange() {
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
setState {
copy(
canInviteByMail = identityServerUrl != null
)
}
}
}
init { init {
val identityServerUrl = identityService.getCurrentIdentityServerUrl()
setState { setState {
copy( copy(
homeServerName = session.myUserId.getDomain() homeServerName = session.myUserId.getDomain(),
canInviteByMail = identityServerUrl != null
) )
} }
startListenToIdentityManager()
} }
@AssistedFactory @AssistedFactory
@ -62,6 +80,19 @@ class CreateSpaceViewModel @AssistedInject constructor(
fun create(initialState: CreateSpaceState): CreateSpaceViewModel fun create(initialState: CreateSpaceState): CreateSpaceViewModel
} }
private fun startListenToIdentityManager() {
identityService.addListener(identityServerManagerListener)
}
private fun stopListenToIdentityManager() {
identityService.removeListener(identityServerManagerListener)
}
override fun onCleared() {
stopListenToIdentityManager()
super.onCleared()
}
companion object : MvRxViewModelFactory<CreateSpaceViewModel, CreateSpaceState> { companion object : MvRxViewModelFactory<CreateSpaceViewModel, CreateSpaceState> {
override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? { override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? {
@ -84,7 +115,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
override fun handle(action: CreateSpaceAction) { override fun handle(action: CreateSpaceAction) {
when (action) { when (action) {
is CreateSpaceAction.SetRoomType -> { is CreateSpaceAction.SetRoomType -> {
setState { setState {
copy( copy(
step = CreateSpaceState.Step.SetDetails, step = CreateSpaceState.Step.SetDetails,
@ -93,7 +124,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
} }
_viewEvents.post(CreateSpaceEvents.NavigateToDetails) _viewEvents.post(CreateSpaceEvents.NavigateToDetails)
} }
is CreateSpaceAction.NameChanged -> { is CreateSpaceAction.NameChanged -> {
setState { setState {
if (aliasManuallyModified) { if (aliasManuallyModified) {
copy( copy(
@ -113,14 +144,14 @@ class CreateSpaceViewModel @AssistedInject constructor(
} }
} }
} }
is CreateSpaceAction.TopicChanged -> { is CreateSpaceAction.TopicChanged -> {
setState { setState {
copy( copy(
topic = action.topic topic = action.topic
) )
} }
} }
is CreateSpaceAction.SpaceAliasChanged -> { is CreateSpaceAction.SpaceAliasChanged -> {
// This called only when the alias is change manually // This called only when the alias is change manually
// not when programmatically changed via a change on name // not when programmatically changed via a change on name
setState { setState {
@ -131,28 +162,43 @@ class CreateSpaceViewModel @AssistedInject constructor(
) )
} }
} }
CreateSpaceAction.OnBackPressed -> { CreateSpaceAction.OnBackPressed -> {
handleBackNavigation() handleBackNavigation()
} }
CreateSpaceAction.NextFromDetails -> { CreateSpaceAction.NextFromDetails -> {
handleNextFromDetails() handleNextFromDetails()
} }
CreateSpaceAction.NextFromDefaultRooms -> { CreateSpaceAction.NextFromDefaultRooms -> {
handleNextFromDefaultRooms() handleNextFromDefaultRooms()
} }
is CreateSpaceAction.DefaultRoomNameChanged -> { CreateSpaceAction.NextFromAdd3pid -> {
handleNextFrom3pid()
}
is CreateSpaceAction.DefaultRoomNameChanged -> {
setState { setState {
copy( copy(
defaultRooms = (defaultRooms ?: emptyMap()).toMutableMap().apply { defaultRooms = defaultRooms.orEmpty().toMutableMap().apply {
this[action.index] = action.name this[action.index] = action.name
} }
) )
} }
} }
is CreateSpaceAction.SetAvatar -> { is CreateSpaceAction.DefaultInvite3pidChanged -> {
setState {
copy(
default3pidInvite = default3pidInvite.orEmpty().toMutableMap().apply {
this[action.index] = action.email
},
emailValidationResult = emailValidationResult.orEmpty().toMutableMap().apply {
this.remove(action.index)
}
)
}
}
is CreateSpaceAction.SetAvatar -> {
setState { copy(avatarUri = action.uri) } setState { copy(avatarUri = action.uri) }
} }
is CreateSpaceAction.SetSpaceTopology -> { is CreateSpaceAction.SetSpaceTopology -> {
handleSetTopology(action) handleSetTopology(action)
} }
}.exhaustive }.exhaustive
@ -173,20 +219,20 @@ class CreateSpaceViewModel @AssistedInject constructor(
setState { setState {
copy( copy(
spaceTopology = SpaceTopology.MeAndTeammates, spaceTopology = SpaceTopology.MeAndTeammates,
step = CreateSpaceState.Step.AddRooms step = CreateSpaceState.Step.AddEmailsOrInvites
) )
} }
_viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) _viewEvents.post(CreateSpaceEvents.NavigateToAdd3Pid)
} }
} }
} }
private fun handleBackNavigation() = withState { state -> private fun handleBackNavigation() = withState { state ->
when (state.step) { when (state.step) {
CreateSpaceState.Step.ChooseType -> { CreateSpaceState.Step.ChooseType -> {
_viewEvents.post(CreateSpaceEvents.Dismiss) _viewEvents.post(CreateSpaceEvents.Dismiss)
} }
CreateSpaceState.Step.SetDetails -> { CreateSpaceState.Step.SetDetails -> {
setState { setState {
copy( copy(
step = CreateSpaceState.Step.ChooseType, step = CreateSpaceState.Step.ChooseType,
@ -196,15 +242,15 @@ class CreateSpaceViewModel @AssistedInject constructor(
} }
_viewEvents.post(CreateSpaceEvents.NavigateToChooseType) _viewEvents.post(CreateSpaceEvents.NavigateToChooseType)
} }
CreateSpaceState.Step.AddRooms -> { CreateSpaceState.Step.AddRooms -> {
if (state.spaceType == SpaceType.Private && state.spaceTopology == SpaceTopology.MeAndTeammates) { if (state.spaceType == SpaceType.Private && state.spaceTopology == SpaceTopology.MeAndTeammates) {
setState { setState {
copy( copy(
spaceTopology = null, spaceTopology = null,
step = CreateSpaceState.Step.ChoosePrivateType step = CreateSpaceState.Step.AddEmailsOrInvites
) )
} }
_viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType) _viewEvents.post(CreateSpaceEvents.NavigateToAdd3Pid)
} else { } else {
setState { setState {
copy( copy(
@ -214,7 +260,7 @@ class CreateSpaceViewModel @AssistedInject constructor(
_viewEvents.post(CreateSpaceEvents.NavigateToDetails) _viewEvents.post(CreateSpaceEvents.NavigateToDetails)
} }
} }
CreateSpaceState.Step.ChoosePrivateType -> { CreateSpaceState.Step.ChoosePrivateType -> {
setState { setState {
copy( copy(
step = CreateSpaceState.Step.SetDetails step = CreateSpaceState.Step.SetDetails
@ -222,6 +268,36 @@ class CreateSpaceViewModel @AssistedInject constructor(
} }
_viewEvents.post(CreateSpaceEvents.NavigateToDetails) _viewEvents.post(CreateSpaceEvents.NavigateToDetails)
} }
CreateSpaceState.Step.AddEmailsOrInvites -> {
setState {
copy(
step = CreateSpaceState.Step.ChoosePrivateType
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToChoosePrivateType)
}
}
}
private fun handleNextFrom3pid() = withState { state ->
// check if emails are valid
val emailValidation = state.default3pidInvite?.mapValues {
val email = it.value
email.isNullOrEmpty() || email.isEmail()
}
if (emailValidation?.all { it.value } != false) {
setState {
copy(
step = CreateSpaceState.Step.AddRooms
)
}
_viewEvents.post(CreateSpaceEvents.NavigateToAddRooms)
} else {
setState {
copy(
emailValidationResult = emailValidation
)
}
} }
} }
@ -296,8 +372,14 @@ class CreateSpaceViewModel @AssistedInject constructor(
defaultRooms = state.defaultRooms defaultRooms = state.defaultRooms
?.entries ?.entries
?.sortedBy { it.key } ?.sortedBy { it.key }
?.mapNotNull { it.value } ?: emptyList(), ?.mapNotNull { it.value }
spaceAlias = alias .orEmpty(),
spaceAlias = alias,
defaultEmailToInvite = state.default3pidInvite
?.values
?.mapNotNull { it.takeIf { it?.isEmail() == true } }
?.takeIf { state.spaceTopology == SpaceTopology.MeAndTeammates }
.orEmpty()
) )
) )
when (result) { when (result) {

View file

@ -25,12 +25,17 @@ import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.api.session.identity.ThreePid
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.GuestAccess
import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent
import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility import org.matrix.android.sdk.api.session.room.model.RoomDirectoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility
import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesAllowEntry
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset import org.matrix.android.sdk.api.session.room.model.create.RestrictedRoomPreset
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -49,7 +54,8 @@ data class CreateSpaceTaskParams(
val spaceAvatar: Uri? = null, val spaceAvatar: Uri? = null,
val spaceAlias: String? = null, val spaceAlias: String? = null,
val isPublic: Boolean, val isPublic: Boolean,
val defaultRooms: List<String> = emptyList() val defaultRooms: List<String> = emptyList(),
val defaultEmailToInvite: List<String> = emptyList()
) )
class CreateSpaceViewModelTask @Inject constructor( class CreateSpaceViewModelTask @Inject constructor(
@ -60,13 +66,28 @@ class CreateSpaceViewModelTask @Inject constructor(
override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult { override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult {
val spaceID = try { val spaceID = try {
session.spaceService().createSpace( session.spaceService().createSpace(CreateSpaceParams().apply {
params.spaceName, this.name = params.spaceName
params.spaceTopic, this.topic = params.spaceTopic
params.spaceAvatar, this.avatarUri = params.spaceAvatar
params.isPublic, if (params.isPublic) {
params.spaceAlias this.roomAliasName = params.spaceAlias
) this.powerLevelContentOverride = (powerLevelContentOverride ?: PowerLevelsContent()).copy(
invite = 0
)
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
this.historyVisibility = RoomHistoryVisibility.WORLD_READABLE
this.guestAccess = GuestAccess.CanJoin
} else {
this.preset = CreateRoomPreset.PRESET_PRIVATE_CHAT
visibility = RoomDirectoryVisibility.PRIVATE
this.invite3pids.addAll(
params.defaultEmailToInvite.map {
ThreePid.Email(it)
}
)
}
})
} catch (failure: Throwable) { } catch (failure: Throwable) {
return CreateSpaceTaskResult.FailedToCreateSpace(failure) return CreateSpaceTaskResult.FailedToCreateSpace(failure)
} }

View file

@ -0,0 +1,99 @@
/*
* Copyright (c) 2021 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.spaces.create
import android.text.InputType
import com.airbnb.epoxy.TypedEpoxyController
import com.google.android.material.textfield.TextInputLayout
import im.vector.app.R
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.ItemStyle
import im.vector.app.core.ui.list.genericButtonItem
import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericPillItem
import im.vector.app.features.form.formEditTextItem
import javax.inject.Inject
class SpaceAdd3pidEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider
) : TypedEpoxyController<CreateSpaceState>() {
var listener: Listener? = null
override fun buildModels(data: CreateSpaceState?) {
val host = this
data ?: return
genericFooterItem {
id("info_help_header")
style(ItemStyle.TITLE)
text(host.stringProvider.getString(R.string.create_spaces_invite_public_header))
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
}
genericFooterItem {
id("info_help_desc")
text(host.stringProvider.getString(R.string.create_spaces_invite_public_header_desc, data.name ?: ""))
textColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_secondary))
}
if (data.canInviteByMail) {
buildEmailFields(data, host)
} else {
genericPillItem {
id("no_IDS")
imageRes(R.drawable.ic_baseline_perm_contact_calendar_24)
text(host.stringProvider.getString(R.string.create_space_identity_server_info_none))
}
genericButtonItem {
id("Discover_Settings")
text(host.stringProvider.getString(R.string.open_discovery_settings))
textColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
buttonClickAction {
host.listener?.onNoIdentityServer()
}
}
}
}
private fun buildEmailFields(data: CreateSpaceState, host: SpaceAdd3pidEpoxyController) {
for (index in 0..2) {
val mail = data.default3pidInvite?.get(index)
formEditTextItem {
id("3pid$index")
enabled(true)
value(mail)
hint(host.stringProvider.getString(R.string.medium_email))
inputType(InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS)
endIconMode(TextInputLayout.END_ICON_CLEAR_TEXT)
errorMessage(
if (data.emailValidationResult?.get(index) == false) {
host.stringProvider.getString(R.string.does_not_look_like_valid_email)
} else null
)
onTextChange { text ->
host.listener?.on3pidChange(index, text)
}
}
}
}
interface Listener {
fun on3pidChange(index: Int, newName: String)
fun onNoIdentityServer()
}
}

View file

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorSurface"
android:orientation="vertical"
android:padding="16dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:importantForAccessibility="no"
android:src="@drawable/ic_beta_pill" />
<TextView
style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="12dp"
android:gravity="center"
android:text="@string/teammate_spaces_arent_quite_ready"
android:textColor="?vctr_content_primary"
android:textStyle="bold" />
<TextView
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:gravity="center"
android:text="@string/teammate_spaces_might_not_join"
android:textColor="?vctr_content_secondary" />
<Button
android:id="@+id/continueButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/layout_vertical_margin"
android:text="@string/continue_anyway"
android:textAllCaps="true" />
</LinearLayout>

View file

@ -2316,6 +2316,7 @@
<string name="identity_server">Identity server</string> <string name="identity_server">Identity server</string>
<string name="disconnect_identity_server">Disconnect identity server</string> <string name="disconnect_identity_server">Disconnect identity server</string>
<string name="add_identity_server">Configure identity server</string> <string name="add_identity_server">Configure identity server</string>
<string name="open_discovery_settings">Open Discovery Settings</string>
<string name="change_identity_server">Change identity server</string> <string name="change_identity_server">Change identity server</string>
<string name="settings_discovery_identity_server_info">You are currently using %1$s to discover and be discoverable by existing contacts you know.</string> <string name="settings_discovery_identity_server_info">You are currently using %1$s to discover and be discoverable by existing contacts you know.</string>
<string name="settings_discovery_identity_server_info_none">You are not currently using an identity server. To discover and be discoverable by existing contacts you know, configure one below.</string> <string name="settings_discovery_identity_server_info_none">You are not currently using an identity server. To discover and be discoverable by existing contacts you know, configure one below.</string>
@ -2538,6 +2539,7 @@
<string name="login_set_email_mandatory_hint">Email</string> <string name="login_set_email_mandatory_hint">Email</string>
<string name="login_set_email_optional_hint">Email (optional)</string> <string name="login_set_email_optional_hint">Email (optional)</string>
<string name="login_set_email_submit">Next</string> <string name="login_set_email_submit">Next</string>
<string name="does_not_look_like_valid_email">Doesn\'t look like a valid email address</string>
<string name="login_set_msisdn_title">Set phone number</string> <string name="login_set_msisdn_title">Set phone number</string>
<string name="login_set_msisdn_notice">Set a phone number to optionally allow people you know to discover you.</string> <string name="login_set_msisdn_notice">Set a phone number to optionally allow people you know to discover you.</string>
@ -3423,6 +3425,8 @@
<string name="create_space_error_empty_field_space_name">Give it a name to continue.</string> <string name="create_space_error_empty_field_space_name">Give it a name to continue.</string>
<string name="create_spaces_room_public_header">What are some discussions you want to have in %s?</string> <string name="create_spaces_room_public_header">What are some discussions you want to have in %s?</string>
<string name="create_spaces_room_public_header_desc">Well create rooms for them. You can add more later too.</string> <string name="create_spaces_room_public_header_desc">Well create rooms for them. You can add more later too.</string>
<string name="create_spaces_invite_public_header">Who are your teammates?</string>
<string name="create_spaces_invite_public_header_desc">Ensure the right people have access to %s company. You can invite more later.</string>
<string name="create_spaces_room_private_header">What things are you working on?</string> <string name="create_spaces_room_private_header">What things are you working on?</string>
<string name="create_spaces_room_private_header_desc">Lets create a room for each of them. You can add more later too, including already existing ones.</string> <string name="create_spaces_room_private_header_desc">Lets create a room for each of them. You can add more later too, including already existing ones.</string>
<string name="create_spaces_default_public_room_name">General</string> <string name="create_spaces_default_public_room_name">General</string>
@ -3448,6 +3452,9 @@
<string name="join_anyway">Join Anyway</string> <string name="join_anyway">Join Anyway</string>
<string name="room_alias_preview_not_found">This alias is not accessible at this time.\nTry again later, or ask a room admin to check if you have access.</string> <string name="room_alias_preview_not_found">This alias is not accessible at this time.\nTry again later, or ask a room admin to check if you have access.</string>
<string name="create_space_identity_server_info_none">You are not currently using an identity server. In order to invite teammates and be discoverable by them, configure one below.</string>
<string name="suggested_rooms_pills_on_empty_text">Youre not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right.</string> <string name="suggested_rooms_pills_on_empty_text">Youre not in any rooms yet. Below are some suggested rooms, but you can see more with the green button bottom right.</string>
<!-- First one is the space name, and the second one is user name --> <!-- First one is the space name, and the second one is user name -->
<string name="suggested_rooms_pills_on_empty_header">Welcome to %1$s, %2$s.</string> <string name="suggested_rooms_pills_on_empty_header">Welcome to %1$s, %2$s.</string>