Space Create Wizard Flow

This commit is contained in:
Valere 2021-02-25 00:23:53 +01:00
parent 6c69a6055d
commit 7d2d7b411e
27 changed files with 481 additions and 68 deletions

View file

@ -21,7 +21,7 @@ import org.matrix.android.sdk.api.failure.MatrixError
import org.matrix.android.sdk.api.session.room.alias.RoomAliasError
sealed class CreateRoomFailure : Failure.FeatureFailure() {
object CreatedWithTimeout : CreateRoomFailure()
data class CreatedWithTimeout(val roomID: String) : CreateRoomFailure()
data class CreatedWithFederationFailure(val matrixError: MatrixError) : CreateRoomFailure()
data class AliasError(val aliasError: RoomAliasError) : CreateRoomFailure()
}

View file

@ -17,11 +17,17 @@
package org.matrix.android.sdk.api.session.space
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.model.RoomSummary
interface Space {
fun asRoom() : Room
/**
* A current snapshot of [RoomSummary] associated with the room
*/
fun spaceSummary(): SpaceSummary?
suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean = false)
suspend fun removeRoom(roomId: String)

View file

@ -66,8 +66,13 @@ internal class DefaultRoomService @Inject constructor(
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource
) : RoomService {
override suspend fun createRoom(createRoomParams: CreateRoomParams): String {
return createRoomTask.execute(createRoomParams)
override fun createRoom(createRoomParams: CreateRoomParams, callback: MatrixCallback<String>): Cancelable {
return createRoomTask
.configureWith(createRoomParams) {
this.callback = callback
this.retryCount = 3
}
.executeBy(taskExecutor)
}
override fun getRoom(roomId: String): Room? {

View file

@ -0,0 +1,39 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.room
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.internal.session.space.DefaultSpace
import org.matrix.android.sdk.internal.session.space.SpaceSummaryDataSource
import javax.inject.Inject
internal interface SpaceGetter {
fun get(spaceId: String): Space?
}
internal class DefaultSpaceGetter @Inject constructor(
private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource
) : SpaceGetter {
override fun get(spaceId: String): Space? {
return roomGetter.getRoom(spaceId)
?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE }
?.let { DefaultSpace(it, spaceSummaryDataSource) }
}
}

View file

@ -102,7 +102,7 @@ internal class DefaultCreateRoomTask @Inject constructor(
.equalTo(RoomSummaryEntityFields.MEMBERSHIP_STR, Membership.JOIN.name)
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout
throw CreateRoomFailure.CreatedWithTimeout(roomId)
}
Realm.getInstance(realmConfiguration).executeTransactionAsync {

View file

@ -73,7 +73,7 @@ internal class DefaultStateService @AssistedInject constructor(@Assisted private
eventType = eventType,
body = body.toSafeJson(eventType)
)
sendStateTask.execute(params)
sendStateTask.executeRetry(params, 3)
}
private fun JsonDict.toSafeJson(eventType: String): JsonDict {

View file

@ -0,0 +1,56 @@
/*
* Copyright 2021 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.matrix.android.sdk.internal.session.space
import io.realm.RealmConfiguration
import kotlinx.coroutines.TimeoutCancellationException
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomParams
import org.matrix.android.sdk.internal.database.awaitNotEmptyResult
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntity
import org.matrix.android.sdk.internal.database.model.SpaceSummaryEntityFields
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.task.Task
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
* A simple wrapper of create room task that adds waiting for DB entities of spaces
*/
internal interface CreateSpaceTask : Task<CreateRoomParams, String>
internal class DefaultCreateSpaceTask @Inject constructor(
private val createRoomTask: CreateRoomTask,
@SessionDatabase private val realmConfiguration: RealmConfiguration
) : CreateSpaceTask {
override suspend fun execute(params: CreateRoomParams): String {
val spaceId = createRoomTask.execute(params)
try {
awaitNotEmptyResult(realmConfiguration, TimeUnit.MINUTES.toMillis(1L)) { realm ->
realm.where(SpaceSummaryEntity::class.java)
.equalTo(SpaceSummaryEntityFields.SPACE_ID, spaceId)
}
} catch (exception: TimeoutCancellationException) {
throw CreateRoomFailure.CreatedWithTimeout(spaceId)
}
return spaceId
}
}

View file

@ -22,15 +22,19 @@ import org.matrix.android.sdk.api.session.events.model.toContent
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.space.Space
import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import java.lang.IllegalArgumentException
class DefaultSpace(private val room: Room) : Space {
internal class DefaultSpace(private val room: Room, private val spaceSummaryDataSource: SpaceSummaryDataSource) : Space {
override fun asRoom(): Room {
return room
}
override fun spaceSummary(): SpaceSummary? {
return spaceSummaryDataSource.getSpaceSummary(asRoom().roomId)
}
override suspend fun addChildren(roomId: String, viaServers: List<String>, order: String?, autoJoin: Boolean) {
asRoom().sendStateEvent(
eventType = EventType.STATE_SPACE_CHILD,

View file

@ -22,7 +22,6 @@ import com.zhuinden.monarchy.Monarchy
import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomType
import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo
import org.matrix.android.sdk.api.session.room.model.create.CreateRoomPreset
import org.matrix.android.sdk.api.session.space.CreateSpaceParams
@ -32,40 +31,33 @@ import org.matrix.android.sdk.api.session.space.SpaceSummary
import org.matrix.android.sdk.api.session.space.SpaceSummaryQueryParams
import org.matrix.android.sdk.api.session.space.model.SpaceChildContent
import org.matrix.android.sdk.internal.di.SessionDatabase
import org.matrix.android.sdk.internal.session.room.RoomGetter
import org.matrix.android.sdk.internal.session.room.alias.DeleteRoomAliasTask
import org.matrix.android.sdk.internal.session.room.alias.GetRoomIdByAliasTask
import org.matrix.android.sdk.internal.session.room.create.CreateRoomTask
import org.matrix.android.sdk.internal.session.room.membership.RoomChangeMembershipStateDataSource
import org.matrix.android.sdk.internal.session.room.membership.joining.JoinRoomTask
import org.matrix.android.sdk.internal.session.room.SpaceGetter
import org.matrix.android.sdk.internal.session.room.membership.leaving.LeaveRoomTask
import org.matrix.android.sdk.internal.session.room.read.MarkAllRoomsReadTask
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.SpacePeekResult
import org.matrix.android.sdk.internal.session.user.accountdata.UpdateBreadcrumbsTask
import org.matrix.android.sdk.internal.task.TaskExecutor
import javax.inject.Inject
internal class DefaultSpaceService @Inject constructor(
@SessionDatabase private val monarchy: Monarchy,
private val createRoomTask: CreateRoomTask,
private val joinRoomTask: JoinRoomTask,
private val createSpaceTask: CreateSpaceTask,
// private val joinRoomTask: JoinRoomTask,
private val joinSpaceTask: JoinSpaceTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val deleteRoomAliasTask: DeleteRoomAliasTask,
private val roomGetter: RoomGetter,
private val spaceGetter: SpaceGetter,
// private val markAllRoomsReadTask: MarkAllRoomsReadTask,
// private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
// private val roomIdByAliasTask: GetRoomIdByAliasTask,
// private val deleteRoomAliasTask: DeleteRoomAliasTask,
// private val roomGetter: RoomGetter,
private val spaceSummaryDataSource: SpaceSummaryDataSource,
private val peekSpaceTask: PeekSpaceTask,
private val resolveSpaceInfoTask: ResolveSpaceInfoTask,
private val leaveRoomTask: LeaveRoomTask,
private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
private val taskExecutor: TaskExecutor
private val leaveRoomTask: LeaveRoomTask
// private val roomChangeMembershipStateDataSource: RoomChangeMembershipStateDataSource,
// private val taskExecutor: TaskExecutor
) : SpaceService {
override suspend fun createSpace(params: CreateSpaceParams): String {
return createRoomTask.execute(params)
return createSpaceTask.executeRetry(params, 3)
}
override suspend fun createSpace(name: String, topic: String?, avatarUri: Uri?, isPublic: Boolean): String {
@ -78,9 +70,7 @@ internal class DefaultSpaceService @Inject constructor(
}
override fun getSpace(spaceId: String): Space? {
return roomGetter.getRoom(spaceId)
?.takeIf { it.roomSummary()?.roomType == RoomType.SPACE }
?.let { DefaultSpace(it) }
return spaceGetter.get(spaceId)
}
override fun getSpaceSummariesLive(queryParams: SpaceSummaryQueryParams): LiveData<List<SpaceSummary>> {

View file

@ -20,6 +20,8 @@ import dagger.Binds
import dagger.Module
import dagger.Provides
import org.matrix.android.sdk.internal.session.SessionScope
import org.matrix.android.sdk.internal.session.room.DefaultSpaceGetter
import org.matrix.android.sdk.internal.session.room.SpaceGetter
import org.matrix.android.sdk.internal.session.space.peeking.DefaultPeekSpaceTask
import org.matrix.android.sdk.internal.session.space.peeking.PeekSpaceTask
import retrofit2.Retrofit
@ -45,4 +47,10 @@ internal abstract class SpaceModule {
@Binds
abstract fun bindJoinSpaceTask(task: DefaultJoinSpaceTask): JoinSpaceTask
@Binds
abstract fun bindSpaceGetter(getter: DefaultSpaceGetter): SpaceGetter
@Binds
abstract fun bindCreateSpaceTask(getter: DefaultCreateSpaceTask): CreateSpaceTask
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.form
import android.text.Editable
import android.view.View
import android.view.inputmethod.EditorInfo
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
@ -50,6 +51,12 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
@EpoxyAttribute
var inputType: Int? = null
@EpoxyAttribute
var singleLine: Boolean? = null
@EpoxyAttribute
var imeOptions: Int? = null
@EpoxyAttribute
var onTextChange: ((String) -> Unit)? = null
@ -69,6 +76,8 @@ abstract class FormEditTextItem : VectorEpoxyModel<FormEditTextItem.Holder>() {
holder.textInputEditText.setTextSafe(value)
holder.textInputEditText.isEnabled = enabled
inputType?.let { holder.textInputEditText.inputType = it }
holder.textInputEditText.isSingleLine = singleLine ?: false
holder.textInputEditText.imeOptions = imeOptions ?: EditorInfo.IME_ACTION_NONE
holder.textInputEditText.addTextChangedListener(onTextChangeListener)
holder.bottomSeparator.isVisible = showBottomSeparator

View file

@ -16,13 +16,16 @@
package im.vector.app.features.form
import android.net.Uri
import android.util.TypedValue
import android.view.View
import android.widget.ImageView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.VectorEpoxyHolder
@ -55,14 +58,25 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder<FormEditableS
override fun bind(holder: Holder) {
super.bind(holder)
holder.imageContainer.onClick(clickListener?.takeIf { enabled })
if (matrixItem != null) {
avatarRenderer?.renderSpace(matrixItem!!, holder.image)
} else {
when {
imageUri != null -> {
val corner = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
8f,
holder.view.resources.displayMetrics
).toInt()
GlideApp.with(holder.image)
.load(imageUri)
.apply(RequestOptions.circleCropTransform())
.transform(MultiTransformation(CenterCrop(), RoundedCorners(corner)))
.into(holder.image)
}
matrixItem != null -> {
avatarRenderer?.renderSpace(matrixItem!!, holder.image)
}
else -> {
avatarRenderer?.clear(holder.image)
}
}
holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true)
holder.delete.onClick(deleteListener?.takeIf { enabled })
}
@ -72,6 +86,7 @@ abstract class FormEditableSquareAvatarItem : EpoxyModelWithHolder<FormEditableS
GlideApp.with(holder.image).clear(holder.image)
super.unbind(holder)
}
class Holder : VectorEpoxyHolder() {
val imageContainer by bind<View>(R.id.itemEditableAvatarImageContainer)
val image by bind<ImageView>(R.id.itemEditableAvatarImage)

View file

@ -35,6 +35,10 @@ abstract class HomeSpaceSummaryItem : VectorEpoxyModel<HomeSpaceSummaryItem.Hold
@EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null
override fun getViewType(): Int {
// mm.. it's reusing the same layout for basic space item
return R.id.space_item_home
}
override fun bind(holder: Holder) {
super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.home
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -36,6 +37,7 @@ import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.replaceFragment
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
@ -103,6 +105,23 @@ class HomeActivity :
@Inject lateinit var avatarRenderer: AvatarRenderer
@Inject lateinit var initSyncStepFormatter: InitSyncStepFormatter
private val createSpaceResultLauncher = registerStartForActivityResult { activityResult ->
if (activityResult.resultCode == Activity.RESULT_OK) {
val spaceId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_CREATED_SPACE_ID)
val defaultRoomsId = activityResult.data?.extras?.getString(SpaceCreationActivity.RESULT_DATA_DEFAULT_ROOM_ID)
views.drawerLayout.closeDrawer(GravityCompat.START)
// Here we want to change current space to the newly created one, and then immediatly open the default room
if (spaceId != null) {
navigator.switchToSpace(this, spaceId, defaultRoomsId)
}
// Also we should show the share space bottomsheet
} else {
// viewModel.handle(CrossSigningSettingsAction.ReAuthCancelled)
}
}
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
@ -147,7 +166,7 @@ class HomeActivity :
startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId))
}
is HomeActivitySharedAction.AddSpace -> {
startActivity(SpaceCreationActivity.newIntent(this))
createSpaceResultLauncher.launch(SpaceCreationActivity.newIntent(this))
}
}.exhaustive
}

View file

@ -29,6 +29,7 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.core.app.TaskStackBuilder
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import arrow.core.Option
import im.vector.app.R
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.core.error.fatalError
@ -46,6 +47,7 @@ import im.vector.app.features.crypto.verification.SupportedVerificationMethodsPr
import im.vector.app.features.crypto.verification.VerificationBottomSheet
import im.vector.app.features.debug.DebugMenuActivity
import im.vector.app.features.devtools.RoomDevToolActivity
import im.vector.app.features.grouplist.SelectedSpaceDataSource
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs
import im.vector.app.features.home.room.detail.search.SearchActivity
@ -77,6 +79,7 @@ import org.matrix.android.sdk.api.session.room.model.thirdparty.RoomDirectoryDat
import org.matrix.android.sdk.api.session.terms.TermsService
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@ -85,6 +88,7 @@ class DefaultNavigator @Inject constructor(
private val sessionHolder: ActiveSessionHolder,
private val vectorPreferences: VectorPreferences,
private val widgetArgsBuilder: WidgetArgsBuilder,
private val selectedSpaceDataSource: SelectedSpaceDataSource,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider
) : Navigator {
@ -98,6 +102,23 @@ class DefaultNavigator @Inject constructor(
startActivity(context, intent, buildTask)
}
override fun switchToSpace(context: Context, spaceId: String, roomId: String?) {
if (sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId) == null) {
fatalError("Trying to open an unknown space $spaceId", vectorPreferences.failFast())
return
}
sessionHolder.getSafeActiveSession()?.spaceService()?.getSpace(spaceId)?.spaceSummary()?.let {
Timber.d("## Nav: Switching to space $spaceId / ${it.roomSummary.name}")
selectedSpaceDataSource.post(Option.just(it))
} ?: kotlin.run {
Timber.d("## Nav: Failed to switch to space $spaceId")
}
if (roomId != null) {
openRoom(context, roomId)
}
}
override fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String) {
val session = sessionHolder.getSafeActiveSession() ?: return
val tx = session.cryptoService().verificationService().getExistingTransaction(otherUserId, sasTransactionId)

View file

@ -38,6 +38,8 @@ interface Navigator {
fun openRoom(context: Context, roomId: String, eventId: String? = null, buildTask: Boolean = false)
fun switchToSpace(context: Context, spaceId: String, roomId: String?)
fun performDeviceVerification(context: Context, otherUserId: String, sasTransactionId: String)
fun requestSessionVerification(context: Context, otherSessionId: String)

View file

@ -20,7 +20,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.Fragment
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
import im.vector.app.R
@ -91,6 +93,23 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
CreateSpaceEvents.NavigateToAddRooms -> {
navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java)
}
is CreateSpaceEvents.ShowModalError -> {
hideWaitingView()
AlertDialog.Builder(this)
.setMessage(it.errorMessage)
.setPositiveButton(getString(R.string.ok), null)
.show()
}
is CreateSpaceEvents.FinishSuccess -> {
setResult(RESULT_OK, Intent().apply {
putExtra(RESULT_DATA_CREATED_SPACE_ID, it.spaceId)
putExtra(RESULT_DATA_DEFAULT_ROOM_ID, it.defaultRoomId)
})
finish()
}
CreateSpaceEvents.HideModalLoading -> {
hideWaitingView()
}
}
}
}
@ -121,9 +140,17 @@ class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Fac
} ?: run {
setTitle(getString(titleRes))
}
if (state.creationResult is Loading) {
showWaitingView(getString(R.string.create_spaces_loading_message))
}
}
companion object {
const val RESULT_DATA_CREATED_SPACE_ID = "RESULT_DATA_CREATED_SPACE_ID"
const val RESULT_DATA_DEFAULT_ROOM_ID = "RESULT_DATA_DEFAULT_ROOM_ID"
fun newIntent(context: Context): Intent {
return Intent(context, SpaceCreationActivity::class.java).apply {
// putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId))

View file

@ -86,8 +86,15 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
private var currentGroupId = ""
init {
observeGroupSummaries()
observeSpaceSummaries()
observeSelectionState()
selectedSpaceDataSource.observe().execute {
if (this.selectedSpace != it.invoke()?.orNull()) {
copy(
selectedSpace = it.invoke()?.orNull()
)
} else this
}
}
private fun observeSelectionState() {
@ -143,8 +150,8 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
_viewEvents.post(SpaceListViewEvents.AddSpace)
}
private fun observeGroupSummaries() {
val roomSummaryQueryParams = roomSummaryQueryParams() {
private fun observeSpaceSummaries() {
val spaceSummaryQueryParams = roomSummaryQueryParams() {
memberships = listOf(Membership.JOIN, Membership.INVITE)
displayName = QueryStringValue.IsNotEmpty
excludeType = listOf(/**RoomType.MESSAGING,$*/
@ -171,7 +178,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp
},
session
.rx()
.liveSpaceSummaries(roomSummaryQueryParams),
.liveSpaceSummaries(spaceSummaryQueryParams),
BiFunction { allCommunityGroup, communityGroups ->
listOf(allCommunityGroup) + communityGroups
}

View file

@ -43,7 +43,7 @@ class ChooseSpaceTypeFragment @Inject constructor(
}))
views.privateButton.setOnClickListener(DebouncedClickListener({
sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private))
// sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private))
}))
}
}

View file

@ -46,10 +46,14 @@ class CreateSpaceDefaultRoomsFragment @Inject constructor(
}
views.nextButton.debouncedClicks {
sharedViewModel.handle(CreateSpaceAction.NextFromDetails)
sharedViewModel.handle(CreateSpaceAction.NextFromDefaultRooms)
}
}
override fun onNameChange(index: Int, newName: String) {
sharedViewModel.handle(CreateSpaceAction.DefaultRoomNameChanged(index, newName))
}
// -----------------------------
// Epoxy controller listener methods
// -----------------------------

View file

@ -16,25 +16,32 @@
package im.vector.app.features.spaces.create
import android.net.Uri
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.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.extensions.configureWith
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding
import javax.inject.Inject
class CreateSpaceDetailsFragment @Inject constructor(
private val epoxyController: SpaceDetailEpoxyController
) : VectorBaseFragment<FragmentSpaceCreateGenericEpoxyFormBinding>(), SpaceDetailEpoxyController.Listener {
private val epoxyController: SpaceDetailEpoxyController,
private val colorProvider: ColorProvider
) : VectorBaseFragment<FragmentSpaceCreateGenericEpoxyFormBinding>(), SpaceDetailEpoxyController.Listener,
GalleryOrCameraDialogHelper.Listener {
private val sharedViewModel: CreateSpaceViewModel by activityViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false)
private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -50,14 +57,19 @@ class CreateSpaceDetailsFragment @Inject constructor(
}
}
override fun onImageReady(uri: Uri?) {
sharedViewModel.handle(CreateSpaceAction.SetAvatar(uri))
}
// -----------------------------
// Epoxy controller listener methods
// -----------------------------
override fun onAvatarDelete() {
sharedViewModel.handle(CreateSpaceAction.SetAvatar(null))
}
override fun onAvatarChange() {
galleryOrCameraDialogHelper.show()
}
override fun onNameChange(newName: String) {

View file

@ -17,20 +17,29 @@
package im.vector.app.features.spaces.create
import android.net.Uri
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.FragmentViewModelContext
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.R
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.core.resources.StringProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
data class CreateSpaceState(
@ -40,7 +49,8 @@ data class CreateSpaceState(
val step: Step = Step.ChooseType,
val spaceType: SpaceType? = null,
val nameInlineError: String? = null,
val defaultRooms: List<String>? = null
val defaultRooms: Map<Int, String?>? = null,
val creationResult: Async<String> = Uninitialized
) : MvRxState {
enum class Step {
@ -59,8 +69,11 @@ sealed class CreateSpaceAction : VectorViewModelAction {
data class SetRoomType(val type: SpaceType) : CreateSpaceAction()
data class NameChanged(val name: String) : CreateSpaceAction()
data class TopicChanged(val topic: String) : CreateSpaceAction()
data class SetAvatar(val uri: Uri?) : CreateSpaceAction()
object OnBackPressed : CreateSpaceAction()
object NextFromDetails : CreateSpaceAction()
object NextFromDefaultRooms : CreateSpaceAction()
data class DefaultRoomNameChanged(val index: Int, val name: String) : CreateSpaceAction()
}
sealed class CreateSpaceEvents : VectorViewEvents {
@ -68,12 +81,17 @@ sealed class CreateSpaceEvents : VectorViewEvents {
object NavigateToChooseType : CreateSpaceEvents()
object NavigateToAddRooms : CreateSpaceEvents()
object Dismiss : CreateSpaceEvents()
data class FinishSuccess(val spaceId: String, val defaultRoomId: String?) : CreateSpaceEvents()
data class ShowModalError(val errorMessage: String) : CreateSpaceEvents()
object HideModalLoading : CreateSpaceEvents()
}
class CreateSpaceViewModel @AssistedInject constructor(
@Assisted initialState: CreateSpaceState,
private val session: Session,
private val stringProvider: StringProvider
private val stringProvider: StringProvider,
private val createSpaceViewModelTask: CreateSpaceViewModelTask,
private val errorFormatter: ErrorFormatter
) : VectorViewModel<CreateSpaceState, CreateSpaceAction, CreateSpaceEvents>(initialState) {
@AssistedFactory
@ -90,6 +108,12 @@ class CreateSpaceViewModel @AssistedInject constructor(
}
return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface")
}
override fun initialState(viewModelContext: ViewModelContext): CreateSpaceState? {
return CreateSpaceState(
defaultRooms = mapOf(0 to viewModelContext.activity.getString(R.string.create_spaces_default_public_room_name))
)
}
}
override fun handle(action: CreateSpaceAction) {
@ -124,6 +148,21 @@ class CreateSpaceViewModel @AssistedInject constructor(
CreateSpaceAction.NextFromDetails -> {
handleNextFromDetails()
}
CreateSpaceAction.NextFromDefaultRooms -> {
handleNextFromDefaultRooms()
}
is CreateSpaceAction.DefaultRoomNameChanged -> {
setState {
copy(
defaultRooms = (defaultRooms ?: emptyMap()).toMutableMap().apply {
this[action.index] = action.name
}
)
}
}
is CreateSpaceAction.SetAvatar -> {
setState { copy(avatarUri = action.uri) }
}
}.exhaustive
}
@ -167,4 +206,53 @@ class CreateSpaceViewModel @AssistedInject constructor(
_viewEvents.post(CreateSpaceEvents.NavigateToAddRooms)
}
}
private fun handleNextFromDefaultRooms() = withState { state ->
val spaceName = state.name ?: return@withState
setState {
copy(creationResult = Loading())
}
viewModelScope.launch(Dispatchers.IO) {
try {
val result = createSpaceViewModelTask.execute(
CreateSpaceTaskParams(
spaceName = spaceName,
spaceTopic = state.topic,
spaceAvatar = state.avatarUri,
isPublic = state.spaceType == SpaceType.Public,
defaultRooms = state.defaultRooms
?.entries
?.sortedBy { it.key }
?.mapNotNull { it.value } ?: emptyList()
)
)
when (result) {
is CreateSpaceTaskResult.Success -> {
setState {
copy(creationResult = Success(result.spaceId))
}
_viewEvents.post(CreateSpaceEvents.FinishSuccess(result.spaceId, result.childIds.firstOrNull()))
}
is CreateSpaceTaskResult.PartialSuccess -> {
// XXX what can we do here?
setState {
copy(creationResult = Success(result.spaceId))
}
_viewEvents.post(CreateSpaceEvents.FinishSuccess(result.spaceId, result.childIds.firstOrNull()))
}
is CreateSpaceTaskResult.FailedToCreateSpace -> {
setState {
copy(creationResult = Fail(result.failure))
}
_viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(result.failure)))
}
}
} catch (failure: Throwable) {
setState {
copy(creationResult = Fail(failure))
}
_viewEvents.post(CreateSpaceEvents.ShowModalError(errorFormatter.toHumanReadable(failure)))
}
}
}
}

View file

@ -0,0 +1,95 @@
/*
* 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.net.Uri
import im.vector.app.core.platform.ViewModelTask
import im.vector.app.core.resources.StringProvider
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.failure.CreateRoomFailure
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.internal.util.awaitCallback
import timber.log.Timber
import javax.inject.Inject
sealed class CreateSpaceTaskResult {
data class Success(val spaceId: String, val childIds: List<String>) : CreateSpaceTaskResult()
data class PartialSuccess(val spaceId: String, val childIds: List<String>, val failedRooms: Map<String, Throwable>) : CreateSpaceTaskResult()
class FailedToCreateSpace(val failure: Throwable) : CreateSpaceTaskResult()
}
data class CreateSpaceTaskParams(
val spaceName: String,
val spaceTopic: String?,
val spaceAvatar: Uri? = null,
val isPublic: Boolean,
val defaultRooms: List<String> = emptyList()
)
class CreateSpaceViewModelTask @Inject constructor(
private val session: Session,
private val stringProvider: StringProvider
) : ViewModelTask<CreateSpaceTaskParams, CreateSpaceTaskResult> {
override suspend fun execute(params: CreateSpaceTaskParams): CreateSpaceTaskResult {
val spaceID = try {
session.spaceService().createSpace(params.spaceName, params.spaceTopic, params.spaceAvatar, params.isPublic)
} catch (failure: Throwable) {
return CreateSpaceTaskResult.FailedToCreateSpace(failure)
}
val createdSpace = session.spaceService().getSpace(spaceID)
val childErrors = mutableMapOf<String, Throwable>()
val childIds = mutableListOf<String>()
if (params.isPublic) {
params.defaultRooms
.filter { it.isNotBlank() }
.forEach { roomName ->
try {
val roomId = try {
awaitCallback<String> {
session.createRoom(CreateRoomParams().apply {
this.name = roomName
this.preset = CreateRoomPreset.PRESET_PUBLIC_CHAT
}, it)
}
} catch (timeout: CreateRoomFailure.CreatedWithTimeout) {
// we ignore that?
timeout.roomID
}
val via = session.sessionParams.homeServerHost?.let { listOf(it) } ?: emptyList()
createdSpace!!.addChildren(roomId, via, null, true)
childIds.add(roomId)
} catch (failure: Throwable) {
Timber.d("Failed to create child room in $spaceID")
childErrors[roomName] = failure
}
}
}
return if (childErrors.isEmpty()) {
CreateSpaceTaskResult.Success(spaceID, childIds)
} else {
CreateSpaceTaskResult.PartialSuccess(spaceID, childIds, childErrors)
}
}
}

View file

@ -28,7 +28,6 @@ import javax.inject.Inject
class SpaceDefaultRoomEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider
// private val avatarRenderer: AvatarRenderer
) : TypedEpoxyController<CreateSpaceState>() {
var listener: Listener? = null
@ -50,36 +49,33 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
formEditTextItem {
id("roomName1")
enabled(true)
value(data?.name)
hint(stringProvider.getString(R.string.create_room_name_hint))
value(data?.defaultRooms?.get(0))
hint(stringProvider.getString(R.string.create_room_name_section))
showBottomSeparator(false)
// errorMessage(data?.nameInlineError)
onTextChange { text ->
// listener?.onNameChange(text)
listener?.onNameChange(0, text)
}
}
formEditTextItem {
id("roomName2")
enabled(true)
// value(data?.name)
hint(stringProvider.getString(R.string.create_room_name_hint))
value(data?.defaultRooms?.get(1))
hint(stringProvider.getString(R.string.create_room_name_section))
showBottomSeparator(false)
// errorMessage(data?.nameInlineError)
onTextChange { text ->
// listener?.onNameChange(text)
listener?.onNameChange(1, text)
}
}
formEditTextItem {
id("roomName3")
enabled(true)
// value(data?.name)
hint(stringProvider.getString(R.string.create_room_name_hint))
value(data?.defaultRooms?.get(2))
hint(stringProvider.getString(R.string.create_room_name_section))
showBottomSeparator(false)
// errorMessage(data?.nameInlineError)
onTextChange { text ->
// listener?.onNameChange(text)
listener?.onNameChange(2, text)
}
}
}
@ -87,7 +83,7 @@ class SpaceDefaultRoomEpoxyController @Inject constructor(
interface Listener {
// fun onAvatarDelete()
// fun onAvatarChange()
// fun onNameChange(newName: String)
fun onNameChange(index: Int, newName: String)
// fun onTopicChange(newTopic: String)
}
}

View file

@ -18,6 +18,7 @@ package im.vector.app.features.spaces.create
import android.content.Context
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.util.TypedValue
import androidx.appcompat.content.res.AppCompatResources.getDrawable
@ -85,7 +86,7 @@ class WizardButtonView @JvmOverloads constructor(context: Context, attrs: Attrib
val outValue = TypedValue()
context.theme.resolveAttribute(android.R.attr.selectableItemBackground, outValue, true)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
this.foreground = getDrawable(context, outValue.resourceId)
}

View file

@ -0,0 +1,3 @@
<resources>
<item type="id" name="space_item_home" />
</resources>

View file

@ -3275,6 +3275,8 @@
<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 Runners World?</string>
<string name="create_spaces_room_public_header_desc">Well create rooms for them, and auto-join everyone. You can add more later too.</string>
<string name="create_spaces_default_public_room_name">General</string>
<string name="create_spaces_loading_message">Creating Space…</string>
</resources>