From 5be3faf914f0c77533fc7cbeac0604fd8d3df751 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 10 May 2021 09:07:38 +0200 Subject: [PATCH 1/5] Epoxy Form fixes --- .../im/vector/app/core/extensions/EditText.kt | 12 ------------ .../vector/app/features/form/FormEditTextItem.kt | 12 ++++++++---- .../features/form/FormEditTextWithButtonItem.kt | 9 ++++++--- .../features/form/FormEditableSquareAvatarItem.kt | 1 - .../features/form/FormMultiLineEditTextItem.kt | 10 +++++++--- .../im/vector/app/features/form/FormSwitchItem.kt | 15 ++++++++++----- .../roomdirectory/createroom/RoomAliasEditItem.kt | 9 ++++++--- vector/src/main/res/layout/item_form_switch.xml | 2 +- 8 files changed, 38 insertions(+), 32 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt index 33e7199334..05b70def3d 100644 --- a/vector/src/main/java/im/vector/app/core/extensions/EditText.kt +++ b/vector/src/main/java/im/vector/app/core/extensions/EditText.kt @@ -57,15 +57,3 @@ fun EditText.setupAsSearch(@DrawableRes searchIconRes: Int = R.drawable.ic_searc return@OnTouchListener false }) } - -/** - * Update the edit text value, only if necessary and move the cursor to the end of the text - */ -fun EditText.setTextSafe(value: String?) { - if (value != null && text.toString() != value) { - setText(value) - // To fix jumping cursor to the start https://github.com/airbnb/epoxy/issues/426 - // Note: there is still a known bug if deleting char in the middle of the text, by long pressing on the backspace button. - setSelection(value.length) - } -} diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt index fdac8afaed..74f088d739 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextItem.kt @@ -27,7 +27,6 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input) @@ -60,7 +59,7 @@ abstract class FormEditTextItem : VectorEpoxyModel() { @EpoxyAttribute var endIconMode: Int? = null - @EpoxyAttribute + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var onTextChange: ((String) -> Unit)? = null private val onTextChangeListener = object : SimpleTextWatcher() { @@ -76,8 +75,13 @@ abstract class FormEditTextItem : VectorEpoxyModel() { holder.textInputLayout.error = errorMessage holder.textInputLayout.endIconMode = endIconMode ?: TextInputLayout.END_ICON_NONE - // Update only if text is different and value is not null - holder.textInputEditText.setTextSafe(value) + if (holder.view.isAttachedToWindow) { + // the view is attached to the window + // So it is a rebind of new data and you could ignore it assuming this is text that was already inputted into the view. + // Downside is if you ever wanted to programmatically change the content of the edit text while it is on screen you would not be able to + } else { + holder.textInputEditText.setText(value) + } holder.textInputEditText.isEnabled = enabled inputType?.let { holder.textInputEditText.inputType = it } holder.textInputEditText.isSingleLine = singleLine ?: false diff --git a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt index 08fc435e11..227e93d2d4 100644 --- a/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt +++ b/vector/src/main/java/im/vector/app/features/form/FormEditTextWithButtonItem.kt @@ -26,7 +26,6 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_form_text_input_with_button) @@ -61,8 +60,12 @@ abstract class FormEditTextWithButtonItem : VectorEpoxyModel Unit)? = null private val onTextChangeListener = object : SimpleTextWatcher() { @@ -77,7 +76,12 @@ abstract class FormMultiLineEditTextItem : VectorEpoxyModel() { var switchChecked: Boolean = false @EpoxyAttribute - var title: String? = null + var title: CharSequence? = null @EpoxyAttribute var summary: String? = null @@ -61,10 +61,15 @@ abstract class FormSwitchItem : VectorEpoxyModel() { holder.switchView.isEnabled = enabled - holder.switchView.setOnCheckedChangeListener(null) - holder.switchView.isChecked = switchChecked - holder.switchView.setOnCheckedChangeListener { _, isChecked -> - listener?.invoke(isChecked) + if (holder.view.isAttachedToWindow) { + // the view is attached to the window + // So it is a rebind of new data and you could ignore it assuming this is value that was already inputted into the view. + } else { + holder.switchView.setOnCheckedChangeListener(null) + holder.switchView.isChecked = switchChecked + holder.switchView.setOnCheckedChangeListener { _, isChecked -> + listener?.invoke(isChecked) + } } holder.divider.isVisible = showDivider } diff --git a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt index 2a30545a47..11f75ddfa7 100644 --- a/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt +++ b/vector/src/main/java/im/vector/app/features/roomdirectory/createroom/RoomAliasEditItem.kt @@ -27,7 +27,6 @@ import com.google.android.material.textfield.TextInputLayout import im.vector.app.R import im.vector.app.core.epoxy.VectorEpoxyHolder import im.vector.app.core.epoxy.VectorEpoxyModel -import im.vector.app.core.extensions.setTextSafe import im.vector.app.core.platform.SimpleTextWatcher @EpoxyModelClass(layout = R.layout.item_room_alias_text_input) @@ -62,8 +61,12 @@ abstract class RoomAliasEditItem : VectorEpoxyModel() holder.textInputLayout.isEnabled = enabled holder.textInputLayout.error = errorMessage - // Update only if text is different and value is not null - holder.textInputEditText.setTextSafe(value) + if (holder.view.isAttachedToWindow) { + // the view is attached to the window + // So it is a rebind of new data and you could ignore it assuming this is text that was already inputted into the view. + } else { + holder.textInputEditText.setText(value) + } holder.textInputEditText.isEnabled = enabled holder.textInputEditText.addTextChangedListener(onTextChangeListener) holder.homeServerText.text = homeServer diff --git a/vector/src/main/res/layout/item_form_switch.xml b/vector/src/main/res/layout/item_form_switch.xml index cc662680bb..54eabd703d 100644 --- a/vector/src/main/res/layout/item_form_switch.xml +++ b/vector/src/main/res/layout/item_form_switch.xml @@ -50,7 +50,7 @@ android:id="@+id/formSwitchDivider" android:layout_width="0dp" android:layout_height="1dp" - android:background="?riotx_header_panel_border_mobile" + android:background="?vctr_list_divider_color" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> From f9e608a790ed377218b15b776fc7d1f97922ff03 Mon Sep 17 00:00:00 2001 From: Valere Date: Mon, 10 May 2021 09:37:54 +0200 Subject: [PATCH 2/5] Manage Spaces as Admin --- .../api/session/room/model/SpaceChildInfo.kt | 3 +- .../android/sdk/api/session/space/Space.kt | 3 + .../database/RealmSessionStoreMigration.kt | 4 + .../database/mapper/RoomSummaryMapper.kt | 4 +- .../database/model/SpaceChildSummaryEntity.kt | 2 + .../internal/session/space/DefaultSpace.kt | 49 +++- .../session/space/DefaultSpaceService.kt | 3 +- .../im/vector/app/core/di/FragmentModule.kt | 12 + .../features/navigation/DefaultNavigator.kt | 3 +- .../settings/RoomSettingsController.kt | 23 +- .../settings/RoomSettingsFragment.kt | 7 +- .../settings/RoomSettingsViewModel.kt | 12 +- .../settings/RoomSettingsViewState.kt | 27 +- .../spaces/SpaceSettingsMenuBottomSheet.kt | 14 +- .../spaces/manage/RoomManageSelectionItem.kt | 72 +++++ .../spaces/manage/SpaceAddRoomsViewModel.kt | 10 +- .../spaces/manage/SpaceManageActivity.kt | 85 ++++-- .../manage/SpaceManageRoomViewAction.kt | 28 ++ .../manage/SpaceManageRoomViewEvents.kt | 24 ++ .../spaces/manage/SpaceManageRoomViewState.kt | 36 +++ .../manage/SpaceManageRoomsController.kt | 101 +++++++ .../spaces/manage/SpaceManageRoomsFragment.kt | 200 +++++++++++++ .../manage/SpaceManageRoomsViewModel.kt | 186 ++++++++++++ .../manage/SpaceManageSharedViewModel.kt | 1 + .../spaces/manage/SpaceManageViewState.kt | 11 +- .../spaces/manage/SpaceManagedSharedAction.kt | 1 + .../manage/SpaceManagedSharedViewEvents.kt | 1 + .../spaces/manage/SpaceSettingsController.kt | 173 ++++++++++++ .../spaces/manage/SpaceSettingsFragment.kt | 265 ++++++++++++++++++ .../layout/bottom_sheet_space_settings.xml | 4 +- .../layout/fragment_room_setting_generic.xml | 2 +- .../layout/item_room_to_manage_in_space.xml | 73 +++++ .../src/main/res/menu/menu_manage_space.xml | 21 ++ vector/src/main/res/values/strings.xml | 6 + vector/src/main/res/values/styles_riot.xml | 9 +- vector/src/main/res/values/theme_dark.xml | 2 + vector/src/main/res/values/theme_light.xml | 3 + .../xml/vector_settings_advanced_settings.xml | 1 + 38 files changed, 1411 insertions(+), 70 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/RoomManageSelectionItem.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewAction.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewEvents.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt create mode 100644 vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt create mode 100644 vector/src/main/res/layout/item_room_to_manage_in_space.xml create mode 100644 vector/src/main/res/menu/menu_manage_space.xml diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt index fd5fbf7bb0..2d31930b33 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/SpaceChildInfo.kt @@ -29,5 +29,6 @@ data class SpaceChildInfo( val activeMemberCount: Int?, val autoJoin: Boolean, val viaServers: List, - val parentRoomId: String? + val parentRoomId: String?, + val suggested: Boolean? ) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt index 9dba4f90af..21daf0b37d 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/space/Space.kt @@ -46,5 +46,8 @@ interface Space { @Throws suspend fun setChildrenAutoJoin(roomId: String, autoJoin: Boolean) + @Throws + suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) + // fun getChildren() : List } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt index 906a6d14d6..211059a345 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/RealmSessionStoreMigration.kt @@ -269,5 +269,9 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration { obj.setString(RoomSummaryEntityFields.JOIN_RULES_STR, roomJoinRules?.name) } + + realm.schema.get("SpaceChildSummaryEntity") + ?.addField(SpaceChildSummaryEntityFields.SUGGESTED, Boolean::class.java) + ?.setNullable(SpaceChildSummaryEntityFields.SUGGESTED, true) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt index 0b057ec6fc..d52e1f995b 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/mapper/RoomSummaryMapper.kt @@ -89,7 +89,9 @@ internal class RoomSummaryMapper @Inject constructor(private val timelineEventMa order = it.order, autoJoin = it.autoJoin ?: false, viaServers = it.viaServers.toList(), - parentRoomId = roomSummaryEntity.roomId + parentRoomId = roomSummaryEntity.roomId, + // What to do here? + suggested = it.suggested ) }, flattenParentIds = roomSummaryEntity.flattenParentIds?.split("|") ?: emptyList() diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt index 982c9ece6a..ce1afbb507 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/model/SpaceChildSummaryEntity.kt @@ -29,6 +29,8 @@ internal open class SpaceChildSummaryEntity( var autoJoin: Boolean? = null, + var suggested: Boolean? = null, + var childRoomId: String? = null, // Link to the actual space summary if it is known locally var childSummaryEntity: RoomSummaryEntity? = null, diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt index 93cb9d9d34..d6df991245 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpace.kt @@ -63,20 +63,21 @@ internal class DefaultSpace( } override suspend fun removeChildren(roomId: String) { - val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) - .firstOrNull() - ?.content.toModel() - ?: // should we throw here? - return +// val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) +// .firstOrNull() +// ?.content.toModel() +// ?: // should we throw here? +// return // edit state event and set via to null room.sendStateEvent( eventType = EventType.STATE_SPACE_CHILD, stateKey = roomId, body = SpaceChildContent( - order = existing.order, + order = null, via = null, - autoJoin = existing.autoJoin + autoJoin = null, + suggested = null ).toContent() ) } @@ -94,7 +95,8 @@ internal class DefaultSpace( body = SpaceChildContent( order = order, via = existing.via, - autoJoin = existing.autoJoin + autoJoin = existing.autoJoin, + suggested = existing.suggested ).toContent() ) } @@ -105,6 +107,11 @@ internal class DefaultSpace( ?.content.toModel() ?: throw IllegalArgumentException("$roomId is not a child of this space") + if (existing.autoJoin == autoJoin) { + // nothing to do? + return + } + // edit state event and set via to null room.sendStateEvent( eventType = EventType.STATE_SPACE_CHILD, @@ -112,7 +119,31 @@ internal class DefaultSpace( body = SpaceChildContent( order = existing.order, via = existing.via, - autoJoin = autoJoin + autoJoin = autoJoin, + suggested = existing.suggested + ).toContent() + ) + } + + override suspend fun setChildrenSuggested(roomId: String, suggested: Boolean) { + val existing = room.getStateEvents(setOf(EventType.STATE_SPACE_CHILD), QueryStringValue.Equals(roomId)) + .firstOrNull() + ?.content.toModel() + ?: throw IllegalArgumentException("$roomId is not a child of this space") + + if (existing.suggested == suggested) { + // nothing to do? + return + } + // edit state event and set via to null + room.sendStateEvent( + eventType = EventType.STATE_SPACE_CHILD, + stateKey = roomId, + body = SpaceChildContent( + order = existing.order, + via = existing.via, + autoJoin = existing.autoJoin, + suggested = suggested ).toContent() ) } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt index c68f4db146..f7fd77c528 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/space/DefaultSpaceService.kt @@ -145,7 +145,8 @@ internal class DefaultSpaceService @Inject constructor( autoJoin = childStateEvContent.autoJoin ?: false, viaServers = childStateEvContent.via.orEmpty(), activeMemberCount = childSummary.numJoinedMembers, - parentRoomId = childStateEv.roomId + parentRoomId = childStateEv.roomId, + suggested = childStateEvContent.suggested ) } }.orEmpty() diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt index c685231756..25c8a07597 100644 --- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt +++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt @@ -125,6 +125,8 @@ import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment import im.vector.app.features.spaces.explore.SpaceDirectoryFragment import im.vector.app.features.spaces.manage.SpaceAddRoomFragment +import im.vector.app.features.spaces.manage.SpaceManageRoomsFragment +import im.vector.app.features.spaces.manage.SpaceSettingsFragment import im.vector.app.features.spaces.people.SpacePeopleFragment import im.vector.app.features.spaces.preview.SpacePreviewFragment import im.vector.app.features.terms.ReviewTermsFragment @@ -684,4 +686,14 @@ interface FragmentModule { @IntoMap @FragmentKey(SpacePeopleFragment::class) fun bindSpacePeopleFragment(fragment: SpacePeopleFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SpaceSettingsFragment::class) + fun bindSpaceSettingsFragment(fragment: SpaceSettingsFragment): Fragment + + @Binds + @IntoMap + @FragmentKey(SpaceManageRoomsFragment::class) + fun bindSpaceManageRoomsFragment(fragment: SpaceManageRoomsFragment): Fragment } diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index 27de04210a..bf8fa497ff 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -74,6 +74,7 @@ import im.vector.app.features.share.SharedData import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpacePreviewActivity +import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity import im.vector.app.features.spaces.people.SpacePeopleActivity import im.vector.app.features.terms.ReviewTermsActivity @@ -123,7 +124,7 @@ class DefaultNavigator @Inject constructor( } } Navigator.PostSwitchSpaceAction.OpenAddExistingRooms -> { - startActivity(context, SpaceManageActivity.newIntent(context, spaceId), false) + startActivity(context, SpaceManageActivity.newIntent(context, spaceId, ManageType.AddRooms), false) } is Navigator.PostSwitchSpaceAction.OpenDefaultRoom -> { val args = RoomDetailArgs( diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt index 299c8ed6ec..a2a8fcb0c2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsController.kt @@ -27,6 +27,7 @@ import im.vector.app.features.form.formEditableAvatarItem import im.vector.app.features.form.formSwitchItem import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.room.detail.timeline.format.RoomHistoryVisibilityFormatter +import im.vector.app.features.roomprofile.settings.RoomSettingsViewState.Companion.getJoinRuleWording import im.vector.app.features.settings.VectorPreferences import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomJoinRules @@ -121,7 +122,7 @@ class RoomSettingsController @Inject constructor( buildProfileAction( id = "joinRule", title = stringProvider.getString(R.string.room_settings_room_access_title), - subtitle = data.getJoinRuleWording(), + subtitle = data.getJoinRuleWording(stringProvider), dividerColor = dividerColor, divider = false, editable = data.actionPermissions.canChangeJoinRule, @@ -142,24 +143,4 @@ class RoomSettingsController @Inject constructor( } } } - - private fun RoomSettingsViewState.getJoinRuleWording(): String { - return when (val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules) { - RoomJoinRules.INVITE -> { - stringProvider.getString(R.string.room_settings_room_access_private_title) - } - RoomJoinRules.PUBLIC -> { - stringProvider.getString(R.string.room_settings_room_access_public_title) - } - RoomJoinRules.KNOCK -> { - stringProvider.getString(R.string.room_settings_room_access_entry_knock) - } - RoomJoinRules.RESTRICTED -> { - stringProvider.getString(R.string.room_settings_room_access_restricted_title) - } - else -> { - stringProvider.getString(R.string.room_settings_room_access_entry_unknown, joinRule.value) - } - } - } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt index 99df27b8f1..9aa164271e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsFragment.kt @@ -60,7 +60,8 @@ class RoomSettingsFragment @Inject constructor( VectorBaseFragment(), RoomSettingsController.Callback, OnBackPressed, - GalleryOrCameraDialogHelper.Listener { + GalleryOrCameraDialogHelper.Listener, + RoomSettingsViewModel.Factory { private val viewModel: RoomSettingsViewModel by fragmentViewModel() private lateinit var roomProfileSharedActionViewModel: RoomProfileSharedActionViewModel @@ -76,6 +77,10 @@ class RoomSettingsFragment @Inject constructor( override fun getMenuRes() = R.menu.vector_room_settings + override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel { + return viewModelFactory.create(initialState) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt index 7f75ded738..0a9c32e4c2 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewModel.kt @@ -17,6 +17,7 @@ package im.vector.app.features.roomprofile.settings import androidx.core.net.toFile +import com.airbnb.mvrx.ActivityViewModelContext import com.airbnb.mvrx.FragmentViewModelContext import com.airbnb.mvrx.MvRxViewModelFactory import com.airbnb.mvrx.ViewModelContext @@ -55,8 +56,11 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: @JvmStatic override fun create(viewModelContext: ViewModelContext, state: RoomSettingsViewState): RoomSettingsViewModel? { - val fragment: RoomSettingsFragment = (viewModelContext as FragmentViewModelContext).fragment() - return fragment.viewModelFactory.create(state) + 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") } } @@ -123,7 +127,9 @@ class RoomSettingsViewModel @AssistedInject constructor(@Assisted initialState: canChangeJoinRule = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_JOIN_RULES) && powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, - EventType.STATE_ROOM_GUEST_ACCESS) + EventType.STATE_ROOM_GUEST_ACCESS), + canAddChildren = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, + EventType.STATE_SPACE_CHILD) ) setState { copy(actionPermissions = permissions) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt index 7403917d48..f1931e4add 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/settings/RoomSettingsViewState.kt @@ -20,6 +20,8 @@ import android.net.Uri import com.airbnb.mvrx.Async import com.airbnb.mvrx.MvRxState import com.airbnb.mvrx.Uninitialized +import im.vector.app.R +import im.vector.app.core.resources.StringProvider import im.vector.app.features.roomprofile.RoomProfileArgs import org.matrix.android.sdk.api.session.room.model.GuestAccess import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility @@ -51,7 +53,8 @@ data class RoomSettingsViewState( val canChangeName: Boolean = false, val canChangeTopic: Boolean = false, val canChangeHistoryVisibility: Boolean = false, - val canChangeJoinRule: Boolean = false + val canChangeJoinRule: Boolean = false, + val canAddChildren: Boolean = false ) sealed class AvatarAction { @@ -67,4 +70,26 @@ data class RoomSettingsViewState( ) { fun hasChanged() = newJoinRules != null || newGuestAccess != null } + + companion object { + fun RoomSettingsViewState.getJoinRuleWording(stringProvider: StringProvider): String { + return when (val joinRule = newRoomJoinRules.newJoinRules ?: currentRoomJoinRules) { + RoomJoinRules.INVITE -> { + stringProvider.getString(R.string.room_settings_room_access_private_title) + } + RoomJoinRules.PUBLIC -> { + stringProvider.getString(R.string.room_settings_room_access_public_title) + } + RoomJoinRules.KNOCK -> { + stringProvider.getString(R.string.room_settings_room_access_entry_knock) + } + RoomJoinRules.RESTRICTED -> { + stringProvider.getString(R.string.room_settings_room_access_restricted_title) + } + else -> { + stringProvider.getString(R.string.room_settings_room_access_entry_unknown, joinRule.value) + } + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 5a1d80c240..c39a9a93fa 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -35,6 +35,7 @@ import im.vector.app.features.navigation.Navigator import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.roomprofile.RoomProfileActivity import im.vector.app.features.settings.VectorPreferences +import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.coroutines.GlobalScope @@ -94,6 +95,13 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { + + @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer + @EpoxyAttribute lateinit var matrixItem: MatrixItem + @EpoxyAttribute var space: Boolean = false + @EpoxyAttribute var selected: Boolean = false + @EpoxyAttribute var suggested: Boolean = false + @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) var itemClickListener: View.OnClickListener? = null + + override fun bind(holder: Holder) { + super.bind(holder) + if (space) { + avatarRenderer.renderSpace(matrixItem, holder.avatarImageView) + } else { + avatarRenderer.render(matrixItem, holder.avatarImageView) + } + holder.titleText.text = matrixItem.getBestName() + + if (selected) { + holder.checkboxImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_checkbox_on)) + holder.checkboxImage.contentDescription = holder.view.context.getString(R.string.a11y_checked) + } else { + holder.checkboxImage.setImageDrawable(ContextCompat.getDrawable(holder.view.context, R.drawable.ic_checkbox_off)) + holder.checkboxImage.contentDescription = holder.view.context.getString(R.string.a11y_unchecked) + } + + holder.suggestedText.isVisible = suggested + + holder.view.setOnClickListener { + itemClickListener?.onClick(it) + } + } + + class Holder : VectorEpoxyHolder() { + val avatarImageView by bind(R.id.itemAddRoomRoomAvatar) + val titleText by bind(R.id.itemAddRoomRoomNameText) + val suggestedText by bind(R.id.itemManageRoomSuggested) + val checkboxImage by bind(R.id.itemAddRoomRoomCheckBox) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt index f64a4c897b..ce677e39c7 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceAddRoomsViewModel.kt @@ -54,6 +54,11 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( private val session: Session ) : VectorViewModel(initialState) { + @AssistedFactory + interface Factory { + fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel + } + val updatableLiveSpacePageResult: UpdatableLivePageResult by lazy { session.getFilteredPagedRoomSummariesLive( roomSummaryQueryParams { @@ -106,11 +111,6 @@ class SpaceAddRoomsViewModel @AssistedInject constructor( } } - @AssistedFactory - interface Factory { - fun create(initialState: SpaceAddRoomsState): SpaceAddRoomsViewModel - } - companion object : MvRxViewModelFactory { override fun create(viewModelContext: ViewModelContext, state: SpaceAddRoomsState): SpaceAddRoomsViewModel? { val factory = when (viewModelContext) { diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt index 47c8982643..f3bf3eda09 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageActivity.kt @@ -20,28 +20,37 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable +import androidx.appcompat.widget.Toolbar +import androidx.core.view.isGone import androidx.core.view.isVisible import com.airbnb.mvrx.MvRx 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.addFragmentToBackstack import im.vector.app.core.extensions.commitTransaction +import im.vector.app.core.extensions.hideKeyboard +import im.vector.app.core.platform.ToolbarConfigurable import im.vector.app.core.platform.VectorBaseActivity -import im.vector.app.databinding.ActivitySimpleBinding +import im.vector.app.databinding.ActivitySimpleLoadingBinding import im.vector.app.features.roomdirectory.RoomDirectorySharedAction import im.vector.app.features.roomdirectory.RoomDirectorySharedActionViewModel import im.vector.app.features.roomdirectory.createroom.CreateRoomArgs import im.vector.app.features.roomdirectory.createroom.CreateRoomFragment +import im.vector.app.features.roomprofile.RoomProfileArgs import kotlinx.parcelize.Parcelize import javax.inject.Inject @Parcelize data class SpaceManageArgs( - val spaceId: String + val spaceId: String, + val manageType: ManageType ) : Parcelable -class SpaceManageActivity : VectorBaseActivity(), SpaceManageSharedViewModel.Factory { +class SpaceManageActivity : VectorBaseActivity(), + ToolbarConfigurable, + SpaceManageSharedViewModel.Factory { @Inject lateinit var sharedViewModelFactory: SpaceManageSharedViewModel.Factory private lateinit var sharedDirectoryActionViewModel: RoomDirectorySharedActionViewModel @@ -50,12 +59,26 @@ class SpaceManageActivity : VectorBaseActivity(), SpaceMa injector.inject(this) } - override fun getBinding(): ActivitySimpleBinding = ActivitySimpleBinding.inflate(layoutInflater) + override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater) override fun getTitleRes(): Int = R.string.space_add_existing_rooms val sharedViewModel: SpaceManageSharedViewModel by viewModel() + override fun showWaitingView(text: String?) { + hideKeyboard() + views.waitingView.waitingStatusText.isGone = views.waitingView.waitingStatusText.text.isNullOrBlank() + super.showWaitingView(text) + } + + override fun hideWaitingView() { + views.waitingView.waitingStatusText.text = null + views.waitingView.waitingStatusText.isGone = true + views.waitingView.waitingHorizontalProgress.progress = 0 + views.waitingView.waitingHorizontalProgress.isVisible = false + super.hideWaitingView() + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -72,14 +95,33 @@ class SpaceManageActivity : VectorBaseActivity(), SpaceMa val args = intent?.getParcelableExtra(MvRx.KEY_ARG) if (isFirstCreation()) { - val simpleName = SpaceAddRoomFragment::class.java.simpleName - if (supportFragmentManager.findFragmentByTag(simpleName) == null) { - supportFragmentManager.commitTransaction { - replace(R.id.simpleFragmentContainer, - SpaceAddRoomFragment::class.java, - Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, - simpleName - ) + withState(sharedViewModel) { + when (it.manageType) { + ManageType.AddRooms -> { + val simpleName = SpaceAddRoomFragment::class.java.simpleName + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpaceAddRoomFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) }, + simpleName + ) + } + } + } + ManageType.Settings -> { + val simpleName = SpaceSettingsFragment::class.java.simpleName + if (supportFragmentManager.findFragmentByTag(simpleName) == null) { + supportFragmentManager.commitTransaction { + replace(R.id.simpleFragmentContainer, + SpaceSettingsFragment::class.java, + Bundle().apply { this.putParcelable(MvRx.KEY_ARG, RoomProfileArgs(args?.spaceId ?: "")) }, + simpleName + ) + } + } + } + ManageType.ManageRooms -> TODO() } } } @@ -90,10 +132,10 @@ class SpaceManageActivity : VectorBaseActivity(), SpaceMa finish() } SpaceManagedSharedViewEvents.HideLoading -> { - views.simpleActivityWaitingView.isVisible = false + hideWaitingView() } SpaceManagedSharedViewEvents.ShowLoading -> { - views.simpleActivityWaitingView.isVisible = true + showWaitingView() } SpaceManagedSharedViewEvents.NavigateToCreateRoom -> { addFragmentToBackstack( @@ -102,17 +144,28 @@ class SpaceManageActivity : VectorBaseActivity(), SpaceMa CreateRoomArgs("", parentSpaceId = args?.spaceId) ) } + SpaceManagedSharedViewEvents.NavigateToManageRooms -> { + addFragmentToBackstack( + R.id.simpleFragmentContainer, + SpaceManageRoomsFragment::class.java, + SpaceManageArgs(args?.spaceId ?: "", ManageType.ManageRooms) + ) + } } } } companion object { - fun newIntent(context: Context, spaceId: String): Intent { + fun newIntent(context: Context, spaceId: String, manageType: ManageType): Intent { return Intent(context, SpaceManageActivity::class.java).apply { - putExtra(MvRx.KEY_ARG, SpaceManageArgs(spaceId)) + putExtra(MvRx.KEY_ARG, SpaceManageArgs(spaceId, manageType)) } } } override fun create(initialState: SpaceManageViewState) = sharedViewModelFactory.create(initialState) + + override fun configure(toolbar: Toolbar) { + configureToolbar(toolbar) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewAction.kt new file mode 100644 index 0000000000..c232377aba --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewAction.kt @@ -0,0 +1,28 @@ +/* + * 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.manage + +import im.vector.app.core.platform.VectorViewModelAction + +sealed class SpaceManageRoomViewAction : VectorViewModelAction { + data class ToggleSelection(val roomId: String) : SpaceManageRoomViewAction() + data class UpdateFilter(val filter: String) : SpaceManageRoomViewAction() + object ClearSelection : SpaceManageRoomViewAction() + data class MarkAllAsSuggested(val suggested: Boolean) : SpaceManageRoomViewAction() + object BulkRemove : SpaceManageRoomViewAction() + object RefreshFromServer : SpaceManageRoomViewAction() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewEvents.kt new file mode 100644 index 0000000000..b135fd89f5 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.manage + +import im.vector.app.core.platform.VectorViewEvents + +sealed class SpaceManageRoomViewEvents : VectorViewEvents { + object BulkActionSuccess: SpaceManageRoomViewEvents() + data class BulkActionFailure(val errorList: List) : SpaceManageRoomViewEvents() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt new file mode 100644 index 0000000000..7bae216b10 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomViewState.kt @@ -0,0 +1,36 @@ +/* + * 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.manage + +import com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo + +data class SpaceManageRoomViewState( + val spaceId: String, + val spaceSummary: Async = Uninitialized, + val childrenInfo: Async> = Uninitialized, + val selectedRooms: List = emptyList(), + val currentFilter: String = "", + val actionState: Async = Uninitialized +) : MvRxState { + constructor(args: SpaceManageArgs) : this( + spaceId = args.spaceId + ) +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt new file mode 100644 index 0000000000..853333faa6 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsController.kt @@ -0,0 +1,101 @@ +/* + * 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.manage + +import com.airbnb.epoxy.TypedEpoxyController +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.Incomplete +import im.vector.app.core.epoxy.errorWithRetryItem +import im.vector.app.core.epoxy.loadingItem +import im.vector.app.core.error.ErrorFormatter +import im.vector.app.core.utils.DebouncedClickListener +import im.vector.app.features.home.AvatarRenderer +import io.reactivex.functions.Predicate +import org.matrix.android.sdk.api.extensions.orFalse +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.util.toMatrixItem +import javax.inject.Inject + +class SpaceManageRoomsController @Inject constructor( + private val avatarRenderer: AvatarRenderer, + private val errorFormatter: ErrorFormatter +) : TypedEpoxyController() { + + interface Listener { + fun toggleSelection(childInfo: SpaceChildInfo) + fun retry() + } + + var listener: Listener? = null + private val matchFilter = SpaceChildInfoMatchFilter() + + override fun buildModels(data: SpaceManageRoomViewState?) { + val roomListAsync = data?.childrenInfo + if (roomListAsync is Incomplete) { + loadingItem { id("loading") } + return + } + if (roomListAsync is Fail) { + errorWithRetryItem { + id("Api Error") + text(errorFormatter.toHumanReadable(roomListAsync.error)) + listener { listener?.retry() } + } + return + } + + val roomList = roomListAsync?.invoke() ?: return + + val directChildren = roomList.filter { + it.parentRoomId == data.spaceId + /** Only direct children **/ + } + matchFilter.filter = data.currentFilter + val filteredResult = directChildren.filter { matchFilter.test(it) } + + filteredResult.forEach { childInfo -> + roomManageSelectionItem { + id(childInfo.childRoomId) + matrixItem(childInfo.toMatrixItem()) + avatarRenderer(avatarRenderer) + suggested(childInfo.suggested ?: false) + space(childInfo.roomType == RoomType.SPACE) + selected(data.selectedRooms.contains(childInfo.childRoomId)) + itemClickListener(DebouncedClickListener({ + listener?.toggleSelection(childInfo) + })) + } + } + } +} + +class SpaceChildInfoMatchFilter : Predicate { + var filter: String = "" + + override fun test(spaceChildInfo: SpaceChildInfo): Boolean { + if (filter.isEmpty()) { + // No filter + return true + } + // if filter is "Jo Do", it should match "John Doe" + return filter.split(" ").all { + spaceChildInfo.name?.contains(it, ignoreCase = true).orFalse() + || spaceChildInfo.topic?.contains(it, ignoreCase = true).orFalse() + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt new file mode 100644 index 0000000000..9e1463cb68 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsFragment.kt @@ -0,0 +1,200 @@ +/* + * 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.manage + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.view.ActionMode +import androidx.appcompat.view.ActionMode.Callback +import androidx.core.view.isVisible +import androidx.transition.TransitionManager +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.appcompat.queryTextChanges +import im.vector.app.R +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.platform.OnBackPressed +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.toast +import im.vector.app.databinding.FragmentSpaceAddRoomsBinding +import io.reactivex.rxkotlin.subscribeBy +import org.matrix.android.sdk.api.session.room.model.SpaceChildInfo +import java.util.concurrent.TimeUnit +import javax.inject.Inject + +class SpaceManageRoomsFragment @Inject constructor( + private val viewModelFactory: SpaceManageRoomsViewModel.Factory, + private val epoxyController: SpaceManageRoomsController +) : VectorBaseFragment(), + SpaceManageRoomsViewModel.Factory, + OnBackPressed, + SpaceManageRoomsController.Listener, + Callback { + + private val viewModel by fragmentViewModel(SpaceManageRoomsViewModel::class) + private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel() + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentSpaceAddRoomsBinding.inflate(inflater) + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + parentFragmentManager.popBackStack() + return true + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.addRoomToSpaceToolbar) + views.appBarTitle.text = getString(R.string.space_manage_rooms_and_spaces) + views.createNewRoom.isVisible = false + epoxyController.listener = this + views.roomList.configureWith(epoxyController, hasFixedSize = true, showDivider = true) + + views.publicRoomsFilter.queryTextChanges() + .debounce(200, TimeUnit.MILLISECONDS) + .subscribeBy { + viewModel.handle(SpaceManageRoomViewAction.UpdateFilter(it.toString())) + } + .disposeOnDestroyView() + + viewModel.selectSubscribe(SpaceManageRoomViewState::actionState) { actionState -> + when (actionState) { + is Loading -> { + sharedViewModel.handle(SpaceManagedSharedAction.ShowLoading) + } + else -> { + sharedViewModel.handle(SpaceManagedSharedAction.HideLoading) + } + } + } + + viewModel.observeViewEvents { + when (it) { + is SpaceManageRoomViewEvents.BulkActionFailure -> { + it.errorList.forEach { + vectorBaseActivity.toast(it) + } + } + SpaceManageRoomViewEvents.BulkActionSuccess -> { + // + } + } + } + } + + override fun onDestroyView() { + epoxyController.listener = null + views.roomList.cleanup() + super.onDestroyView() + } + + override fun create(initialState: SpaceManageRoomViewState) = viewModelFactory.create(initialState) + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + + state.spaceSummary.invoke()?.let { + views.appBarSpaceInfo.text = it.displayName + } + if (state.selectedRooms.isNotEmpty()) { + if (currentActionMode == null) { + views.addRoomToSpaceToolbar.isVisible = true + vectorBaseActivity.startSupportActionMode(this) + } else { + currentActionMode?.title = "${state.selectedRooms.size} selected" + } +// views.addRoomToSpaceToolbar.isVisible = false +// views.addRoomToSpaceToolbar.startActionMode(this) + } else { + currentActionMode?.finish() + } + Unit + } + + var currentActionMode: ActionMode? = null + + override fun toggleSelection(childInfo: SpaceChildInfo) { + viewModel.handle(SpaceManageRoomViewAction.ToggleSelection(childInfo.childRoomId)) + } + + override fun retry() { + viewModel.handle(SpaceManageRoomViewAction.RefreshFromServer) + } + + override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { + val inflater = mode?.menuInflater + inflater?.inflate(R.menu.menu_manage_space, menu) + withState(viewModel) { + mode?.title = resources.getQuantityString(R.plurals.room_details_selected, it.selectedRooms.size, it.selectedRooms.size) + } + currentActionMode = mode + views.addRoomToSpaceToolbar.isVisible = false + return true + } + + override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { + withState(viewModel) { state -> + // check if we show mark as suggested or not + val areAllSuggested = state.childrenInfo.invoke().orEmpty().filter { state.selectedRooms.contains(it.childRoomId) } + .all { it.suggested == true } + menu?.findItem(R.id.action_mark_as_suggested)?.isVisible = !areAllSuggested + menu?.findItem(R.id.action_mark_as_not_suggested)?.isVisible = areAllSuggested + } + + return true + } + + override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.action_delete -> { + handleDeleteSelection() + } + R.id.action_mark_as_suggested -> { + viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(true)) + } + R.id.action_mark_as_not_suggested -> { + viewModel.handle(SpaceManageRoomViewAction.MarkAllAsSuggested(false)) + } + else -> { + } + } + mode?.finish() + return true + } + + private fun handleDeleteSelection() { + viewModel.handle(SpaceManageRoomViewAction.BulkRemove) + } + + override fun onDestroyActionMode(mode: ActionMode?) { + // should force a refresh + currentActionMode = null + viewModel.handle(SpaceManageRoomViewAction.ClearSelection) + views.coordinatorLayout.post { + if (isAdded) { + TransitionManager.beginDelayedTransition(views.coordinatorLayout) + views.addRoomToSpaceToolbar.isVisible = true + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt new file mode 100644 index 0000000000..d229c5072b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageRoomsViewModel.kt @@ -0,0 +1,186 @@ +/* + * 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.manage + +import androidx.lifecycle.viewModelScope +import com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +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.core.error.ErrorFormatter +import im.vector.app.core.mvrx.runCatchingToAsync +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.session.Session + +class SpaceManageRoomsViewModel @AssistedInject constructor( + @Assisted val initialState: SpaceManageRoomViewState, + private val session: Session, + private val errorFormatter: ErrorFormatter +) : VectorViewModel(initialState) { + + init { + val spaceSummary = session.getRoomSummary(initialState.spaceId) + setState { + copy( + spaceSummary = spaceSummary?.let { Success(it) } ?: Uninitialized, + childrenInfo = Loading() + ) + } + + viewModelScope.launch(Dispatchers.IO) { + val apiResult = runCatchingToAsync { + session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second + } + setState { + copy( + childrenInfo = apiResult + ) + } + } + } + + @AssistedFactory + interface Factory { + fun create(initialState: SpaceManageRoomViewState): SpaceManageRoomsViewModel + } + + companion object : MvRxViewModelFactory { + override fun create(viewModelContext: ViewModelContext, state: SpaceManageRoomViewState): SpaceManageRoomsViewModel? { + 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: SpaceManageRoomViewAction) { + when (action) { + is SpaceManageRoomViewAction.ToggleSelection -> handleToggleSelection(action) + is SpaceManageRoomViewAction.UpdateFilter -> { + setState { copy(currentFilter = action.filter) } + } + SpaceManageRoomViewAction.ClearSelection -> { + setState { copy(selectedRooms = emptyList()) } + } + SpaceManageRoomViewAction.BulkRemove -> { + handleBulkRemove() + } + is SpaceManageRoomViewAction.MarkAllAsSuggested -> { + handleBulkMarkAsSuggested(action.suggested) + } + SpaceManageRoomViewAction.RefreshFromServer -> { + refreshSummaryAPI() + } + } + } + + private fun handleBulkRemove() = withState { state -> + setState { copy(actionState = Loading()) } + val selection = state.selectedRooms + session.coroutineScope.launch(Dispatchers.IO) { + val errorList = mutableListOf() + selection.forEach { + try { + session.spaceService().getSpace(state.spaceId)?.removeChildren(it) + } catch (failure: Throwable) { + errorList.add(failure) + } + } + if (errorList.isEmpty()) { + // success + _viewEvents.post(SpaceManageRoomViewEvents.BulkActionSuccess) + } else { + _viewEvents.post(SpaceManageRoomViewEvents.BulkActionFailure(errorList.map { errorFormatter.toHumanReadable(it) })) + } + setState { copy(actionState = Uninitialized) } + } + } + + private fun handleBulkMarkAsSuggested(suggested: Boolean) = withState { state -> + setState { copy(actionState = Loading()) } + val selection = state.childrenInfo.invoke()?.filter { + state.selectedRooms.contains(it.childRoomId) + }.orEmpty() + session.coroutineScope.launch(Dispatchers.IO) { + val errorList = mutableListOf() + selection.forEach { info -> + try { +// session.spaceService().getSpace(state.spaceId)?.setChildrenSuggested(roomId, suggested) + session.spaceService().getSpace(state.spaceId)?.addChildren( + roomId = info.childRoomId, + viaServers = info?.viaServers ?: emptyList(), + order = info?.order, + suggested = suggested, + autoJoin = info?.autoJoin ?: false + ) + } catch (failure: Throwable) { + errorList.add(failure) + } + } + if (errorList.isEmpty()) { + // success + _viewEvents.post(SpaceManageRoomViewEvents.BulkActionSuccess) + refreshSummaryAPI() + } else { + _viewEvents.post(SpaceManageRoomViewEvents.BulkActionFailure(errorList.map { errorFormatter.toHumanReadable(it) })) + } + setState { copy(actionState = Uninitialized) } + } + } + + private fun refreshSummaryAPI() { + setState { + copy( + childrenInfo = Loading() + ) + } + viewModelScope.launch(Dispatchers.IO) { + val apiResult = runCatchingToAsync { + session.spaceService().querySpaceChildren(spaceId = initialState.spaceId).second + } + setState { + copy( + childrenInfo = apiResult + ) + } + } + } + + private fun handleToggleSelection(action: SpaceManageRoomViewAction.ToggleSelection) = withState { state -> + val existing = state.selectedRooms.toMutableList() + if (existing.contains(action.roomId)) { + existing.remove(action.roomId) + } else { + existing.add(action.roomId) + } + setState { + copy( + selectedRooms = existing.toList() + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt index 24110c081a..57c47250f9 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageSharedViewModel.kt @@ -55,6 +55,7 @@ class SpaceManageSharedViewModel @AssistedInject constructor( SpaceManagedSharedAction.HideLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.HideLoading) SpaceManagedSharedAction.ShowLoading -> _viewEvents.post(SpaceManagedSharedViewEvents.ShowLoading) SpaceManagedSharedAction.CreateRoom -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToCreateRoom) + SpaceManagedSharedAction.ManageRooms -> _viewEvents.post(SpaceManagedSharedViewEvents.NavigateToManageRooms) } } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt index 18c289f3b3..91d49d90d1 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManageViewState.kt @@ -18,10 +18,17 @@ package im.vector.app.features.spaces.manage import com.airbnb.mvrx.MvRxState +enum class ManageType { + AddRooms, + Settings, + ManageRooms +} data class SpaceManageViewState( - val spaceId: String = "" + val spaceId: String = "", + val manageType: ManageType ) : MvRxState { constructor(args: SpaceManageArgs) : this( - spaceId = args.spaceId + spaceId = args.spaceId, + manageType = args.manageType ) } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt index 1c0de1041b..0b413a3b8a 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedAction.kt @@ -23,4 +23,5 @@ sealed class SpaceManagedSharedAction : VectorViewModelAction { object ShowLoading : SpaceManagedSharedAction() object HideLoading : SpaceManagedSharedAction() object CreateRoom : SpaceManagedSharedAction() + object ManageRooms : SpaceManagedSharedAction() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt index 0cf6bcaee1..da6f01d205 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceManagedSharedViewEvents.kt @@ -23,4 +23,5 @@ sealed class SpaceManagedSharedViewEvents : VectorViewEvents { object ShowLoading : SpaceManagedSharedViewEvents() object HideLoading : SpaceManagedSharedViewEvents() object NavigateToCreateRoom : SpaceManagedSharedViewEvents() + object NavigateToManageRooms : SpaceManagedSharedViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt new file mode 100644 index 0000000000..c92692a800 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsController.kt @@ -0,0 +1,173 @@ +/* + * 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.manage + +import com.airbnb.epoxy.TypedEpoxyController +import im.vector.app.R +import im.vector.app.core.epoxy.profiles.buildProfileAction +import im.vector.app.core.epoxy.profiles.buildProfileSection +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.StringProvider +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.form.formSwitchItem +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.settings.RoomSettingsViewState +import im.vector.app.features.roomprofile.settings.RoomSettingsViewState.Companion.getJoinRuleWording +import im.vector.app.features.settings.VectorPreferences +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +class SpaceSettingsController @Inject constructor( + private val stringProvider: StringProvider, + private val avatarRenderer: AvatarRenderer, + colorProvider: ColorProvider, + private val vectorPreferences: VectorPreferences +) : TypedEpoxyController() { + + interface Callback { + // Delete the avatar, or cancel an avatar change + fun onAvatarDelete() + fun onAvatarChange() + fun onNameChanged(name: String) + fun onTopicChanged(topic: String) + fun onHistoryVisibilityClicked() + fun onJoinRuleClicked() + fun onToggleGuestAccess() + fun onDevTools() + fun onDevRoomSettings() + fun onManageRooms() + fun setIsPublic(public: Boolean) + } + + private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) + + var callback: Callback? = null + + override fun buildModels(data: RoomSettingsViewState?) { + val roomSummary = data?.roomSummary?.invoke() ?: return + + formEditableSquareAvatarItem { + id("avatar") + enabled(data.actionPermissions.canChangeAvatar) + when (val avatarAction = data.avatarAction) { + RoomSettingsViewState.AvatarAction.None -> { + // Use the current value + avatarRenderer(avatarRenderer) + // We do not want to use the fallback avatar url, which can be the other user avatar, or the current user avatar. + matrixItem(roomSummary.toMatrixItem().copy(avatarUrl = data.currentRoomAvatarUrl)) + } + RoomSettingsViewState.AvatarAction.DeleteAvatar -> + imageUri(null) + is RoomSettingsViewState.AvatarAction.UpdateAvatar -> + imageUri(avatarAction.newAvatarUri) + } + clickListener { callback?.onAvatarChange() } + deleteListener { callback?.onAvatarDelete() } + } + + buildProfileSection( + stringProvider.getString(R.string.settings) + ) + + formEditTextItem { + id("name") + enabled(data.actionPermissions.canChangeName) + value(data.newName ?: roomSummary.displayName) + hint(stringProvider.getString(R.string.create_room_name_hint)) + showBottomSeparator(false) + onTextChange { text -> + callback?.onNameChanged(text) + } + } + + formMultiLineEditTextItem { + id("topic") + enabled(data.actionPermissions.canChangeTopic) + value(data.newTopic ?: roomSummary.topic) + hint(stringProvider.getString(R.string.create_space_topic_hint)) + showBottomSeparator(false) + onTextChange { text -> + callback?.onTopicChanged(text) + } + } + + if (vectorPreferences.labsUseExperimentalRestricted()) { + buildProfileAction( + id = "joinRule", + title = stringProvider.getString(R.string.room_settings_room_access_title), + subtitle = data.getJoinRuleWording(stringProvider), + dividerColor = dividerColor, + divider = true, + editable = data.actionPermissions.canChangeJoinRule, + action = { if (data.actionPermissions.canChangeJoinRule) callback?.onJoinRuleClicked() } + ) + } else { + val isPublic = (data.newRoomJoinRules.newJoinRules ?: data.currentRoomJoinRules) == RoomJoinRules.PUBLIC + formSwitchItem { + id("isPublic") + enabled(data.actionPermissions.canChangeJoinRule) + title(stringProvider.getString(R.string.make_this_space_public)) + switchChecked(isPublic) + + listener { value -> + callback?.setIsPublic(value) + } + } + } + + buildProfileAction( + id = "manage_rooms", + title = stringProvider.getString(R.string.space_settings_manage_rooms), + // subtitle = data.getJoinRuleWording(stringProvider), + dividerColor = dividerColor, + divider = vectorPreferences.developerMode(), + editable = data.actionPermissions.canAddChildren, + action = { + if (data.actionPermissions.canAddChildren) callback?.onManageRooms() + } + ) + + if (vectorPreferences.developerMode()) { + buildProfileAction( + id = "dev_tools", + title = stringProvider.getString(R.string.settings_dev_tools), + icon = R.drawable.ic_verification_glasses, + tintIcon = false, + dividerColor = dividerColor, + divider = true, + action = { + callback?.onDevTools() + } + ) + + buildProfileAction( + id = "room_tools", + title = stringProvider.getString(R.string.room_list_quick_actions_room_settings), + icon = R.drawable.ic_verification_glasses, + tintIcon = false, + dividerColor = dividerColor, + divider = false, + action = { + callback?.onDevRoomSettings() + } + ) + } + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt new file mode 100644 index 0000000000..e33f461f47 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/manage/SpaceSettingsFragment.kt @@ -0,0 +1,265 @@ +/* + * 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.manage + +import android.net.Uri +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import com.airbnb.mvrx.activityViewModel +import com.airbnb.mvrx.args +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState +import im.vector.app.R +import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper +import im.vector.app.core.extensions.cleanup +import im.vector.app.core.extensions.configureWith +import im.vector.app.core.extensions.exhaustive +import im.vector.app.core.intent.getFilenameFromUri +import im.vector.app.core.platform.OnBackPressed +import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.resources.ColorProvider +import im.vector.app.core.resources.DrawableProvider +import im.vector.app.core.utils.toast +import im.vector.app.databinding.FragmentRoomSettingGenericBinding +import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.RoomProfileArgs +import im.vector.app.features.roomprofile.settings.RoomSettingsAction +import im.vector.app.features.roomprofile.settings.RoomSettingsViewEvents +import im.vector.app.features.roomprofile.settings.RoomSettingsViewModel +import im.vector.app.features.roomprofile.settings.RoomSettingsViewState +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleBottomSheet +import im.vector.app.features.roomprofile.settings.joinrule.RoomJoinRuleSharedActionViewModel +import org.matrix.android.sdk.api.session.room.model.GuestAccess +import org.matrix.android.sdk.api.session.room.model.RoomHistoryVisibility +import org.matrix.android.sdk.api.session.room.model.RoomJoinRules +import org.matrix.android.sdk.api.util.toMatrixItem +import java.util.UUID +import javax.inject.Inject + +class SpaceSettingsFragment @Inject constructor( + private val epoxyController: SpaceSettingsController, + private val colorProvider: ColorProvider, + val viewModelFactory: RoomSettingsViewModel.Factory, + private val avatarRenderer: AvatarRenderer, + private val drawableProvider: DrawableProvider +) : VectorBaseFragment(), + RoomSettingsViewModel.Factory, + SpaceSettingsController.Callback, + GalleryOrCameraDialogHelper.Listener, + OnBackPressed { + + private val viewModel: RoomSettingsViewModel by fragmentViewModel() + private val sharedViewModel: SpaceManageSharedViewModel by activityViewModel() + + private lateinit var roomJoinRuleSharedActionViewModel: RoomJoinRuleSharedActionViewModel + + private val galleryOrCameraDialogHelper = GalleryOrCameraDialogHelper(this, colorProvider) + + override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentRoomSettingGenericBinding.inflate(inflater) + + private val roomProfileArgs: RoomProfileArgs by args() + + override fun getMenuRes() = R.menu.vector_room_settings + + override fun create(initialState: RoomSettingsViewState): RoomSettingsViewModel { + return viewModelFactory.create(initialState) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setupToolbar(views.roomSettingsToolbar) + // roomProfileSharedActionViewModel = activityViewModelProvider.get(RoomProfileSharedActionViewModel::class.java) +// setupRoomHistoryVisibilitySharedActionViewModel() + setupRoomJoinRuleSharedActionViewModel() + epoxyController.callback = this + views.roomSettingsRecyclerView.configureWith(epoxyController, hasFixedSize = true) + views.waitingView.waitingStatusText.setText(R.string.please_wait) + views.waitingView.waitingStatusText.isVisible = true + + viewModel.observeViewEvents { + when (it) { + is RoomSettingsViewEvents.Failure -> showFailure(it.throwable) + RoomSettingsViewEvents.Success -> showSuccess() + RoomSettingsViewEvents.GoBack -> { + ignoreChanges = true + vectorBaseActivity.onBackPressed() + } + }.exhaustive + } + } + + override fun onDestroyView() { + epoxyController.callback = null + views.roomSettingsRecyclerView.cleanup() + super.onDestroyView() + } + + override fun onPrepareOptionsMenu(menu: Menu) { + withState(viewModel) { state -> + menu.findItem(R.id.roomSettingsSaveAction).isVisible = state.showSaveAction + } + super.onPrepareOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == R.id.roomSettingsSaveAction) { + viewModel.handle(RoomSettingsAction.Save) + } + return super.onOptionsItemSelected(item) + } + + private fun renderRoomSummary(state: RoomSettingsViewState) { + views.waitingView.root.isVisible = state.isLoading + + state.roomSummary()?.let { + views.roomSettingsToolbarTitleView.text = it.displayName + views.roomSettingsToolbarTitleView.setCompoundDrawablesWithIntrinsicBounds( + null, + null, + drawableProvider.getDrawable(R.drawable.ic_beta_pill), + null + ) + avatarRenderer.renderSpace(it.toMatrixItem(), views.roomSettingsToolbarAvatarImageView) + views.roomSettingsDecorationToolbarAvatarImageView.render(it.roomEncryptionTrustLevel) + } + + invalidateOptionsMenu() + } + + override fun invalidate() = withState(viewModel) { state -> + epoxyController.setData(state) + renderRoomSummary(state) + } + + private fun setupRoomJoinRuleSharedActionViewModel() { + roomJoinRuleSharedActionViewModel = activityViewModelProvider.get(RoomJoinRuleSharedActionViewModel::class.java) + roomJoinRuleSharedActionViewModel + .observe() + .subscribe { action -> + viewModel.handle(RoomSettingsAction.SetRoomJoinRule(action.roomJoinRule)) + } + .disposeOnDestroyView() + } + + private var ignoreChanges = false + + override fun onBackPressed(toolbarButton: Boolean): Boolean { + if (ignoreChanges) return false + + return withState(viewModel) { + return@withState if (it.showSaveAction) { + AlertDialog.Builder(requireContext()) + .setTitle(R.string.dialog_title_warning) + .setMessage(R.string.warning_unsaved_change) + .setPositiveButton(R.string.warning_unsaved_change_discard) { _, _ -> + viewModel.handle(RoomSettingsAction.Cancel) + } + .setNegativeButton(R.string.cancel, null) + .show() + true + } else { + false + } + } + } + + private fun showSuccess() { + activity?.toast(R.string.room_settings_save_success) + } + + override fun onNameChanged(name: String) { + viewModel.handle(RoomSettingsAction.SetRoomName(name)) + } + + override fun onTopicChanged(topic: String) { + viewModel.handle(RoomSettingsAction.SetRoomTopic(topic)) + } + + override fun onHistoryVisibilityClicked() {} + + override fun onJoinRuleClicked() = withState(viewModel) { state -> + val currentJoinRule = state.newRoomJoinRules.newJoinRules ?: state.currentRoomJoinRules + RoomJoinRuleBottomSheet.newInstance(currentJoinRule) + .show(childFragmentManager, "RoomJoinRuleBottomSheet") + } + + override fun onToggleGuestAccess() = withState(viewModel) { state -> + val currentGuestAccess = state.newRoomJoinRules.newGuestAccess ?: state.currentGuestAccess + val toggled = if (currentGuestAccess == GuestAccess.Forbidden) GuestAccess.CanJoin else GuestAccess.Forbidden + viewModel.handle(RoomSettingsAction.SetRoomGuestAccess(toggled)) + } + + override fun onDevTools() = withState(viewModel) { state -> + navigator.openDevTools(requireContext(), state.roomId) + } + + override fun onDevRoomSettings() = withState(viewModel) { state -> + navigator.openRoomProfile(requireContext(), state.roomId) + } + + override fun onManageRooms() { + sharedViewModel.handle(SpaceManagedSharedAction.ManageRooms) + } + + override fun setIsPublic(public: Boolean) { + if (public) { + viewModel.handle(RoomSettingsAction.SetRoomJoinRule(RoomJoinRules.PUBLIC)) + viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(RoomHistoryVisibility.WORLD_READABLE)) + } else { + viewModel.handle(RoomSettingsAction.SetRoomJoinRule(RoomJoinRules.INVITE)) + viewModel.handle(RoomSettingsAction.SetRoomHistoryVisibility(RoomHistoryVisibility.INVITED)) + } + } + + override fun onImageReady(uri: Uri?) { + uri ?: return + viewModel.handle( + RoomSettingsAction.SetAvatarAction( + RoomSettingsViewState.AvatarAction.UpdateAvatar( + newAvatarUri = uri, + newAvatarFileName = getFilenameFromUri(requireContext(), uri) ?: UUID.randomUUID().toString()) + ) + ) + } + + override fun onAvatarDelete() { + withState(viewModel) { + when (it.avatarAction) { + RoomSettingsViewState.AvatarAction.None -> { + viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.DeleteAvatar)) + } + RoomSettingsViewState.AvatarAction.DeleteAvatar -> { + /* Should not happen */ + } + is RoomSettingsViewState.AvatarAction.UpdateAvatar -> { + // Cancel the update of the avatar + viewModel.handle(RoomSettingsAction.SetAvatarAction(RoomSettingsViewState.AvatarAction.None)) + } + } + } + } + + override fun onAvatarChange() { + galleryOrCameraDialogHelper.show() + } +} diff --git a/vector/src/main/res/layout/bottom_sheet_space_settings.xml b/vector/src/main/res/layout/bottom_sheet_space_settings.xml index 053a326b03..b473c7b03d 100644 --- a/vector/src/main/res/layout/bottom_sheet_space_settings.xml +++ b/vector/src/main/res/layout/bottom_sheet_space_settings.xml @@ -94,12 +94,10 @@ android:id="@+id/spaceSettings" android:layout_width="match_parent" android:layout_height="wrap_content" - android:visibility="gone" app:actionTitle="@string/settings" app:leftIcon="@drawable/ic_settings_root_general" app:tint="?attr/riotx_text_primary" - app:titleTextColor="?attr/riotx_text_primary" - tools:visibility="visible" /> + app:titleTextColor="?attr/riotx_text_primary"/> + android:background="?riotx_background"> + + + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/menu/menu_manage_space.xml b/vector/src/main/res/menu/menu_manage_space.xml new file mode 100644 index 0000000000..858c3bc9c3 --- /dev/null +++ b/vector/src/main/res/menu/menu_manage_space.xml @@ -0,0 +1,21 @@ + + + + + + + + + + \ No newline at end of file diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 29a298c821..cea541410b 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -3356,4 +3356,10 @@ %s invites you Looking for someone not in %s? + Manage rooms + Make this space public + Suggested + Mark as suggested + Mark as not suggested + Manage rooms and spaces diff --git a/vector/src/main/res/values/styles_riot.xml b/vector/src/main/res/values/styles_riot.xml index a265727522..c3b8247582 100644 --- a/vector/src/main/res/values/styles_riot.xml +++ b/vector/src/main/res/values/styles_riot.xml @@ -11,6 +11,13 @@ ?riotx_background + + @@ -65,7 +72,7 @@ diff --git a/vector/src/main/res/values/theme_dark.xml b/vector/src/main/res/values/theme_dark.xml index d5b8a40cbb..94252622f4 100644 --- a/vector/src/main/res/values/theme_dark.xml +++ b/vector/src/main/res/values/theme_dark.xml @@ -181,6 +181,8 @@ @android:color/transparent + + @style/ActionModeTheme -