From 4b27ad8ba6b4ced21afa1389f72ad62451b38e24 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 22 Feb 2021 19:59:19 +0100 Subject: [PATCH] Creation wizard WIP --- vector/src/main/AndroidManifest.xml | 1 + .../im/vector/app/core/di/FragmentModule.kt | 18 ++ .../im/vector/app/core/di/ScreenComponent.kt | 2 + .../form/FormEditableSquareAvatarItem.kt | 80 +++++++++ .../vector/app/features/home/HomeActivity.kt | 4 + .../features/home/HomeActivitySharedAction.kt | 1 + .../features/spaces/SpaceCreationActivity.kt | 135 ++++++++++++++ .../app/features/spaces/SpaceListFragment.kt | 5 + .../features/spaces/SpaceSummaryController.kt | 12 ++ .../features/spaces/SpacesListViewModel.kt | 7 + .../spaces/create/ChooseSpaceTypeFragment.kt | 49 +++++ .../create/CreateSpaceDefaultRoomsFragment.kt | 56 ++++++ .../create/CreateSpaceDetailsFragment.kt | 70 ++++++++ .../spaces/create/CreateSpaceViewModel.kt | 170 ++++++++++++++++++ .../create/SpaceDefaultRoomEpoxyController.kt | 93 ++++++++++ .../create/SpaceDetailEpoxyController.kt | 90 ++++++++++ .../spaces/create/WizardButtonView.kt | 98 ++++++++++ .../src/main/res/drawable/ic_camera_plain.xml | 10 ++ .../src/main/res/drawable/ic_public_room.xml | 13 ++ .../src/main/res/drawable/ic_room_private.xml | 10 ++ .../fragment_space_create_choose_type.xml | 71 ++++++++ ...agment_space_create_generic_epoxy_form.xml | 35 ++++ .../layout/item_editable_square_avatar.xml | 67 +++++++ .../res/layout/view_space_type_button.xml | 68 +++++++ vector/src/main/res/values/attrs.xml | 12 +- vector/src/main/res/values/strings.xml | 18 ++ 26 files changed, 1194 insertions(+), 1 deletion(-) create mode 100644 vector/src/main/java/im/vector/app/features/form/FormEditableSquareAvatarItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt create mode 100644 vector/src/main/res/drawable/ic_camera_plain.xml create mode 100644 vector/src/main/res/drawable/ic_public_room.xml create mode 100644 vector/src/main/res/drawable/ic_room_private.xml create mode 100644 vector/src/main/res/layout/fragment_space_create_choose_type.xml create mode 100644 vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml create mode 100644 vector/src/main/res/layout/item_editable_square_avatar.xml create mode 100644 vector/src/main/res/layout/view_space_type_button.xml diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index e66a123773..205669fb8a 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -274,6 +274,7 @@ + () { + + @EpoxyAttribute + var avatarRenderer: AvatarRenderer? = null + + @EpoxyAttribute + var matrixItem: MatrixItem? = null + + @EpoxyAttribute + var enabled: Boolean = true + + @EpoxyAttribute + var imageUri: Uri? = null + + @EpoxyAttribute + var clickListener: ClickListener? = null + + @EpoxyAttribute + var deleteListener: ClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + holder.imageContainer.onClick(clickListener?.takeIf { enabled }) + if (matrixItem != null) { + avatarRenderer?.renderSpace(matrixItem!!, holder.image) + } else { + GlideApp.with(holder.image) + .load(imageUri) + .apply(RequestOptions.circleCropTransform()) + .into(holder.image) + } + holder.delete.isVisible = enabled && (imageUri != null || matrixItem?.avatarUrl?.isNotEmpty() == true) + holder.delete.onClick(deleteListener?.takeIf { enabled }) + } + + override fun unbind(holder: Holder) { + avatarRenderer?.clear(holder.image) + GlideApp.with(holder.image).clear(holder.image) + super.unbind(holder) + } + class Holder : VectorEpoxyHolder() { + val imageContainer by bind(R.id.itemEditableAvatarImageContainer) + val image by bind(R.id.itemEditableAvatarImage) + val delete by bind(R.id.itemEditableAvatarDelete) + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index 138ffc26f4..982b59263d 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -54,6 +54,7 @@ import im.vector.app.features.popup.VerificationVectorAlert import im.vector.app.features.rageshake.VectorUncaughtExceptionHandler import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorSettingsActivity +import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpacePreviewActivity import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.workers.signout.ServerBackupStatusViewModel @@ -145,6 +146,9 @@ class HomeActivity : is HomeActivitySharedAction.OpenSpacePreview -> { startActivity(SpacePreviewActivity.newIntent(this, sharedAction.spaceId)) } + is HomeActivitySharedAction.AddSpace -> { + startActivity(SpaceCreationActivity.newIntent(this)) + } }.exhaustive } .disposeOnDestroy() diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt index f72354465b..72e3523336 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivitySharedAction.kt @@ -25,5 +25,6 @@ sealed class HomeActivitySharedAction : VectorSharedAction { object OpenDrawer : HomeActivitySharedAction() object CloseDrawer : HomeActivitySharedAction() object OpenGroup : HomeActivitySharedAction() + object AddSpace : HomeActivitySharedAction() data class OpenSpacePreview(val spaceId: String) : HomeActivitySharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt new file mode 100644 index 0000000000..65c8102020 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceCreationActivity.kt @@ -0,0 +1,135 @@ +/* + * 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 + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.fragment.app.Fragment +import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +import im.vector.app.core.extensions.toMvRxBundle +import im.vector.app.core.platform.SimpleFragmentActivity +import im.vector.app.features.spaces.create.ChooseSpaceTypeFragment +import im.vector.app.features.spaces.create.CreateSpaceAction +import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment +import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment +import im.vector.app.features.spaces.create.CreateSpaceEvents +import im.vector.app.features.spaces.create.CreateSpaceState +import im.vector.app.features.spaces.create.CreateSpaceViewModel +import javax.inject.Inject + +class SpaceCreationActivity : SimpleFragmentActivity(), CreateSpaceViewModel.Factory { + + @Inject lateinit var viewModelFactory: CreateSpaceViewModel.Factory + + override fun injectWith(injector: ScreenComponent) { + super.injectWith(injector) + injector.inject(this) + } + + val viewModel: CreateSpaceViewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (isFirstCreation()) { + when (withState(viewModel) { it.step }) { + CreateSpaceState.Step.ChooseType -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceState.Step.SetDetails -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceState.Step.AddRooms -> TODO() + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + onBackPressed() + return true + } + return super.onOptionsItemSelected(item) + } + + override fun initUiAndData() { + super.initUiAndData() + viewModel.subscribe(this) { + renderState(it) + } + + viewModel.observeViewEvents { + when (it) { + CreateSpaceEvents.NavigateToDetails -> { + navigateToFragment(CreateSpaceDetailsFragment::class.java) + } + CreateSpaceEvents.NavigateToChooseType -> { + navigateToFragment(ChooseSpaceTypeFragment::class.java) + } + CreateSpaceEvents.Dismiss -> { + finish() + } + CreateSpaceEvents.NavigateToAddRooms -> { + navigateToFragment(CreateSpaceDefaultRoomsFragment::class.java) + } + } + } + } + + private fun navigateToFragment(fragmentClass: Class) { + val frag = supportFragmentManager.findFragmentByTag(fragmentClass.name) ?: createFragment(fragmentClass, Bundle().toMvRxBundle()) + supportFragmentManager.beginTransaction() + .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out) + .replace(R.id.container, + frag, + fragmentClass.name + ) + .commit() + } + + override fun onBackPressed() { + viewModel.handle(CreateSpaceAction.OnBackPressed) + } + + private fun renderState(state: CreateSpaceState) { + val titleRes = when (state.step) { + CreateSpaceState.Step.ChooseType -> R.string.activity_create_space_title + CreateSpaceState.Step.SetDetails -> R.string.your_public_space + CreateSpaceState.Step.AddRooms -> R.string.your_public_space + } + supportActionBar?.let { + it.title = getString(titleRes) + } ?: run { + setTitle(getString(titleRes)) + } + } + + companion object { + fun newIntent(context: Context): Intent { + return Intent(context, SpaceCreationActivity::class.java).apply { + // putExtra(MvRx.KEY_ARG, SpaceDirectoryArgs(spaceId)) + } + } + } + + override fun create(initialState: CreateSpaceState): CreateSpaceViewModel = viewModelFactory.create(initialState) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt index e14b920b2c..bc1af38473 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceListFragment.kt @@ -57,6 +57,7 @@ class SpaceListFragment @Inject constructor( when (it) { is SpaceListViewEvents.OpenSpaceSummary -> sharedActionViewModel.post(HomeActivitySharedAction.OpenSpacePreview(it.id)) is SpaceListViewEvents.OpenSpace -> sharedActionViewModel.post(HomeActivitySharedAction.OpenGroup) + is SpaceListViewEvents.AddSpace -> sharedActionViewModel.post(HomeActivitySharedAction.AddSpace) }.exhaustive } } @@ -78,4 +79,8 @@ class SpaceListFragment @Inject constructor( override fun onSpaceSelected(spaceSummary: SpaceSummary) { viewModel.handle(SpaceListAction.SelectSpace(spaceSummary)) } + + override fun onAddSpaceSelected() { + viewModel.handle(SpaceListAction.AddSpace) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt index 29d48b4cd1..d1bc3c6e1c 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSummaryController.kt @@ -19,8 +19,10 @@ package im.vector.app.features.spaces import com.airbnb.epoxy.EpoxyController import im.vector.app.R import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericButtonItem import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericItemHeader +import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.features.grouplist.homeSpaceSummaryItem import im.vector.app.features.home.AvatarRenderer import org.matrix.android.sdk.api.session.room.model.Membership @@ -103,9 +105,19 @@ class SpaceSummaryController @Inject constructor( } } } + + // Temporary item to create a new Space (will move with final design) + + genericButtonItem { + id("create") + text(stringProvider.getString(R.string.add_space)) + iconRes(R.drawable.ic_add_black) + buttonClickAction(DebouncedClickListener({ callback?.onAddSpaceSelected() })) + } } interface Callback { fun onSpaceSelected(spaceSummary: SpaceSummary) + fun onAddSpaceSelected() } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt index cbfa760f56..b8089d6f98 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpacesListViewModel.kt @@ -46,6 +46,7 @@ const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID" sealed class SpaceListAction : VectorViewModelAction { data class SelectSpace(val spaceSummary: SpaceSummary) : SpaceListAction() + object AddSpace : SpaceListAction() } /** @@ -54,6 +55,7 @@ sealed class SpaceListAction : VectorViewModelAction { sealed class SpaceListViewEvents : VectorViewEvents { object OpenSpace : SpaceListViewEvents() data class OpenSpaceSummary(val id: String) : SpaceListViewEvents() + object AddSpace : SpaceListViewEvents() } data class SpaceListViewState( @@ -110,6 +112,7 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp override fun handle(action: SpaceListAction) { when (action) { is SpaceListAction.SelectSpace -> handleSelectSpace(action) + else -> handleAddSpace() } } @@ -136,6 +139,10 @@ class SpacesListViewModel @AssistedInject constructor(@Assisted initialState: Sp } } + private fun handleAddSpace() { + _viewEvents.post(SpaceListViewEvents.AddSpace) + } + private fun observeGroupSummaries() { val roomSummaryQueryParams = roomSummaryQueryParams() { memberships = listOf(Membership.JOIN, Membership.INVITE) diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt new file mode 100644 index 0000000000..080bb99ea4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/ChooseSpaceTypeFragment.kt @@ -0,0 +1,49 @@ +/* + * 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.core.platform.VectorBaseFragment +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.databinding.FragmentSpaceCreateChooseTypeBinding +import javax.inject.Inject + +class ChooseSpaceTypeFragment @Inject constructor( + // private val viewModelFactory: CreateSpaceViewModel.Factory, +) : VectorBaseFragment() { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateChooseTypeBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.publicButton.setOnClickListener(DebouncedClickListener({ + sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Public)) + })) + + views.privateButton.setOnClickListener(DebouncedClickListener({ + sharedViewModel.handle(CreateSpaceAction.SetRoomType(SpaceType.Private)) + })) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt new file mode 100644 index 0000000000..2dc36f8715 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDefaultRoomsFragment.kt @@ -0,0 +1,56 @@ +/* + * 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.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding +import javax.inject.Inject + +class CreateSpaceDefaultRoomsFragment @Inject constructor( + private val epoxyController: SpaceDefaultRoomEpoxyController +) : VectorBaseFragment(), SpaceDefaultRoomEpoxyController.Listener { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.recyclerView.configureWith(epoxyController) + epoxyController.listener = this + + sharedViewModel.subscribe(this) { + epoxyController.setData(it) + } + + views.nextButton.debouncedClicks { + sharedViewModel.handle(CreateSpaceAction.NextFromDetails) + } + } + + // ----------------------------- + // Epoxy controller listener methods + // ----------------------------- +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt new file mode 100644 index 0000000000..506f71c92f --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceDetailsFragment.kt @@ -0,0 +1,70 @@ +/* + * 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.core.extensions.configureWith +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.databinding.FragmentSpaceCreateGenericEpoxyFormBinding +import javax.inject.Inject + +class CreateSpaceDetailsFragment @Inject constructor( + private val epoxyController: SpaceDetailEpoxyController +) : VectorBaseFragment(), SpaceDetailEpoxyController.Listener { + + private val sharedViewModel: CreateSpaceViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSpaceCreateGenericEpoxyFormBinding.inflate(layoutInflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + views.recyclerView.configureWith(epoxyController) + epoxyController.listener = this + + sharedViewModel.subscribe(this) { + epoxyController.setData(it) + } + + views.nextButton.debouncedClicks { + sharedViewModel.handle(CreateSpaceAction.NextFromDetails) + } + } + + // ----------------------------- + // Epoxy controller listener methods + // ----------------------------- + + override fun onAvatarDelete() { + } + + override fun onAvatarChange() { + } + + override fun onNameChange(newName: String) { + sharedViewModel.handle(CreateSpaceAction.NameChanged(newName)) + } + + override fun onTopicChange(newTopic: String) { + sharedViewModel.handle(CreateSpaceAction.TopicChanged(newTopic)) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt new file mode 100644 index 0000000000..aca8452300 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/CreateSpaceViewModel.kt @@ -0,0 +1,170 @@ +/* + * 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 com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.MvRxViewModelFactory +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.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 org.matrix.android.sdk.api.session.Session + +data class CreateSpaceState( + val name: String? = null, + val avatarUri: Uri? = null, + val topic: String = "", + val step: Step = Step.ChooseType, + val spaceType: SpaceType? = null, + val nameInlineError : String? = null, + val defaultRooms: List? = null +) : MvRxState { + + enum class Step { + ChooseType, + SetDetails, + AddRooms + } +} + +enum class SpaceType { + Public, + Private +} + +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() + object OnBackPressed : CreateSpaceAction() + object NextFromDetails : CreateSpaceAction() +} + +sealed class CreateSpaceEvents : VectorViewEvents { + object NavigateToDetails : CreateSpaceEvents() + object NavigateToChooseType : CreateSpaceEvents() + object NavigateToAddRooms : CreateSpaceEvents() + object Dismiss : CreateSpaceEvents() +} + +class CreateSpaceViewModel @AssistedInject constructor( + @Assisted initialState: CreateSpaceState, + private val session: Session, + private val stringProvider: StringProvider +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: CreateSpaceState): CreateSpaceViewModel + } + + companion object : MvRxViewModelFactory { + + override fun create(viewModelContext: ViewModelContext, state: CreateSpaceState): CreateSpaceViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + override fun handle(action: CreateSpaceAction) { + when (action) { + is CreateSpaceAction.SetRoomType -> { + setState { + copy( + step = CreateSpaceState.Step.SetDetails, + spaceType = action.type + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToDetails) + } + is CreateSpaceAction.NameChanged -> { + setState { + copy( + nameInlineError = null, + name = action.name + ) + } + } + is CreateSpaceAction.TopicChanged -> { + setState { + copy( + topic = action.topic + ) + } + } + CreateSpaceAction.OnBackPressed -> { + handleBackNavigation() + } + CreateSpaceAction.NextFromDetails -> { + handleNextFromDetails() + } + }.exhaustive + } + + private fun handleBackNavigation() = withState { state -> + when (state.step) { + CreateSpaceState.Step.ChooseType -> { + _viewEvents.post(CreateSpaceEvents.Dismiss) + } + CreateSpaceState.Step.SetDetails -> { + setState { + copy( + step = CreateSpaceState.Step.ChooseType + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToChooseType) + } + CreateSpaceState.Step.AddRooms -> { + setState { + copy( + step = CreateSpaceState.Step.SetDetails + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToDetails) + } + } + } + + private fun handleNextFromDetails() = withState { state -> + if (state.name.isNullOrBlank()) { + setState { + copy( + nameInlineError = stringProvider.getString(R.string.create_space_error_empty_field_space_name) + ) + } + } else { + setState { + copy( + step = CreateSpaceState.Step.AddRooms + ) + } + _viewEvents.post(CreateSpaceEvents.NavigateToAddRooms) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt new file mode 100644 index 0000000000..05abcf95b0 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDefaultRoomEpoxyController.kt @@ -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 com.airbnb.epoxy.TypedEpoxyController +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.GenericItem +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.form.formEditTextItem +import javax.inject.Inject + +class SpaceDefaultRoomEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val colorProvider: ColorProvider +// private val avatarRenderer: AvatarRenderer +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: CreateSpaceState?) { + genericFooterItem { + id("info_help_header") + style(GenericItem.STYLE.BIG_TEXT) + text(stringProvider.getString(R.string.create_spaces_room_public_header)) + textColor(colorProvider.getColorFromAttribute(R.attr.riot_primary_text_color)) + } + + genericFooterItem { + id("info_help") + text(stringProvider.getString(R.string.create_spaces_room_public_header_desc)) + textColor(colorProvider.getColorFromAttribute(R.attr.riotx_text_secondary)) + } + + formEditTextItem { + id("roomName1") + enabled(true) + value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + + formEditTextItem { + id("roomName2") + enabled(true) +// value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + + formEditTextItem { + id("roomName3") + enabled(true) +// value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) +// errorMessage(data?.nameInlineError) + onTextChange { text -> +// listener?.onNameChange(text) + } + } + } + + interface Listener { +// fun onAvatarDelete() +// fun onAvatarChange() +// fun onNameChange(newName: String) +// fun onTopicChange(newTopic: String) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt new file mode 100644 index 0000000000..357b741ff4 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/SpaceDetailEpoxyController.kt @@ -0,0 +1,90 @@ +/* + * 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 com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.resources.StringProvider +import im.vector.app.core.ui.list.genericFooterItem +import im.vector.app.features.form.formEditTextItem +import im.vector.app.features.form.formEditableSquareAvatarItem +import im.vector.app.features.form.formMultiLineEditTextItem +import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.util.MatrixItem +import javax.inject.Inject + +class SpaceDetailEpoxyController @Inject constructor( + private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer +) : TypedEpoxyController() { + + var listener: Listener? = null + + override fun buildModels(data: CreateSpaceState?) { + genericFooterItem { + id("info_help") + text( + if (data?.spaceType == SpaceType.Public) { + stringProvider.getString(R.string.create_spaces_details_public_header) + } else { + stringProvider.getString(R.string.create_spaces_details_private_header) + } + ) + } + + formEditableSquareAvatarItem { + id("avatar") + enabled(true) + imageUri(data?.avatarUri) + avatarRenderer(avatarRenderer) + matrixItem(data?.name?.let { MatrixItem.RoomItem("", it, null).takeIf { !it.displayName.isNullOrBlank() } }) + clickListener { listener?.onAvatarChange() } + deleteListener { listener?.onAvatarDelete() } + } + + formEditTextItem { + id("name") + enabled(true) + value(data?.name) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) + errorMessage(data?.nameInlineError) + onTextChange { text -> + listener?.onNameChange(text) + } + } + + formMultiLineEditTextItem { + id("topic") + enabled(true) + value(data?.topic) + hint(stringProvider.getString(R.string.create_room_topic_hint)) + showBottomSeparator(false) + textSizeSp(15) + onTextChange { text -> + listener?.onTopicChange(text) + } + } + } + + interface Listener { + fun onAvatarDelete() + fun onAvatarChange() + fun onNameChange(newName: String) + fun onTopicChange(newTopic: String) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt new file mode 100644 index 0000000000..552a98ded2 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/create/WizardButtonView.kt @@ -0,0 +1,98 @@ +/* + * 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.content.Context +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.util.TypedValue +import androidx.appcompat.content.res.AppCompatResources.getDrawable +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.withStyledAttributes +import im.vector.app.R +import im.vector.app.databinding.ViewSpaceTypeButtonBinding + +class WizardButtonView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) + : ConstraintLayout(context, attrs, defStyle) { + + private val views: ViewSpaceTypeButtonBinding + + var title: String? = null + set(value) { + if (value != title) { + field = value + views.title.text = value + } + } + + var subTitle: String? = null + set(value) { + if (value != subTitle) { + field = value + views.subTitle.text = value + } + } + + var icon: Drawable? = null + set(value) { + if (value != icon) { + field = value + views.buttonImageView.setImageDrawable(value) + } + } + +// private var tint: Int? = null +// set(value) { +// field = value +// if (value != null) { +// views.buttonImageView.imageTintList = ColorStateList.valueOf(value) +// } else { +// views.buttonImageView.clearColorFilter() +// } +// } + +// var action: (() -> Unit)? = null + + init { + inflate(context, R.layout.view_space_type_button, this) + views = ViewSpaceTypeButtonBinding.bind(this) + + if (isInEditMode) { + title = "Title" + subTitle = "This is doing something" + } + + context.withStyledAttributes(attrs, R.styleable.WizardButtonView) { + title = getString(R.styleable.WizardButtonView_title) ?: "" + subTitle = getString(R.styleable.WizardButtonView_subTitle) ?: "" + icon = getDrawable(R.styleable.WizardButtonView_icon) +// tint = getColor(R.styleable.WizardButtonView_iconTint, ThemeUtils.getColor(context, R.attr.riotx_text_primary)) + } + + 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) { + this.foreground = getDrawable(context, outValue.resourceId) + } + +// views.content.isClickable = true +// views.content.isFocusable = true +// views.content.setOnClickListener { +// action?.invoke() +// } + } +} diff --git a/vector/src/main/res/drawable/ic_camera_plain.xml b/vector/src/main/res/drawable/ic_camera_plain.xml new file mode 100644 index 0000000000..56d55c9da1 --- /dev/null +++ b/vector/src/main/res/drawable/ic_camera_plain.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/drawable/ic_public_room.xml b/vector/src/main/res/drawable/ic_public_room.xml new file mode 100644 index 0000000000..1520898831 --- /dev/null +++ b/vector/src/main/res/drawable/ic_public_room.xml @@ -0,0 +1,13 @@ + + + + diff --git a/vector/src/main/res/drawable/ic_room_private.xml b/vector/src/main/res/drawable/ic_room_private.xml new file mode 100644 index 0000000000..cacdf15a3b --- /dev/null +++ b/vector/src/main/res/drawable/ic_room_private.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/fragment_space_create_choose_type.xml b/vector/src/main/res/layout/fragment_space_create_choose_type.xml new file mode 100644 index 0000000000..ddf61aabf8 --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_create_choose_type.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml b/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml new file mode 100644 index 0000000000..3097664e02 --- /dev/null +++ b/vector/src/main/res/layout/fragment_space_create_generic_epoxy_form.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/layout/item_editable_square_avatar.xml b/vector/src/main/res/layout/item_editable_square_avatar.xml new file mode 100644 index 0000000000..b3ec057fd4 --- /dev/null +++ b/vector/src/main/res/layout/item_editable_square_avatar.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/layout/view_space_type_button.xml b/vector/src/main/res/layout/view_space_type_button.xml new file mode 100644 index 0000000000..1ccbed3201 --- /dev/null +++ b/vector/src/main/res/layout/view_space_type_button.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/attrs.xml b/vector/src/main/res/values/attrs.xml index ecc64bf07d..86105cf74d 100644 --- a/vector/src/main/res/values/attrs.xml +++ b/vector/src/main/res/values/attrs.xml @@ -54,8 +54,10 @@ + + - + @@ -68,4 +70,12 @@ + + + + + + + + diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 01adc31bb1..b2bfe76f57 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3259,4 +3259,22 @@ Messages failed to send Delete unsent messages Are you sure you want to delete all unsent messages in this room? + + Add Space + Your public space + Spaces are a new way to group rooms and people + What type of space do you want to create? + To join an existing space, you need an invite. + Public + Open to anyone, best for communities + Private + Invite only, best for yourself or teams + Create a space + Add some details to help it stand out. You can change these at any point. + Add some details to help people identify it. You can change these at any point. + Give it a name to continue. + What are some discussions you want to have in Runner’s World? + We’ll create rooms for them, and auto-join everyone. You can add more later too. + +