From 85417eb24c2dff8039ac3fb5c9a8227ebb7b29b3 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoitm@matrix.org> Date: Fri, 2 Oct 2020 21:12:19 +0200 Subject: [PATCH 01/17] Add SearchView to filter members. Author: Onuray (I squashed 2 commits) --- CHANGES.md | 1 + .../members/RoomMemberListAction.kt | 1 + .../members/RoomMemberListController.kt | 4 ++- .../members/RoomMemberListFragment.kt | 17 +++++++++++++ .../members/RoomMemberListViewModel.kt | 25 +++++++++++++++++++ .../members/RoomMemberListViewState.kt | 1 + .../layout/fragment_room_setting_generic.xml | 14 ++++++++++- 7 files changed, 61 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4a158086a2..f5d6fca073 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,6 +11,7 @@ Improvements 🙌: - Small optimisation of scrolling experience in timeline (#2114) - Allow user to reset cross signing if he has no way to recover (#2052) - Create home shortcut for any room (#1525) + - Filter room member by name (#2184) Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt index c61dcb98f5..342a2e8585 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListAction.kt @@ -20,4 +20,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class RoomMemberListAction : VectorViewModelAction { data class RevokeThreePidInvite(val stateKey: String) : RoomMemberListAction() + data class FilterMemberList(val searchTerm: String) : RoomMemberListAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 59b1bca26f..f0c7e146c6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -53,7 +53,9 @@ class RoomMemberListController @Inject constructor( } override fun buildModels(data: RoomMemberListViewState?) { - val roomMembersByPowerLevel = data?.roomMemberSummaries?.invoke() ?: return + data ?: return + + val roomMembersByPowerLevel = data.filteredRoomMemberSummaries ?: data.roomMemberSummaries.invoke() ?: return val threePidInvites = data.threePidInvites().orEmpty() var threePidInvitesDone = threePidInvites.isEmpty() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 46d0f35b95..e12cce9484 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -21,6 +21,8 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -72,12 +74,27 @@ class RoomMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(roomSettingsToolbar) + setupSearchView() recyclerView.configureWith(roomMemberListController, hasFixedSize = true) viewModel.selectSubscribe(this, RoomMemberListViewState::actionsPermissions) { invalidateOptionsMenu() } } + private fun setupSearchView() { + searchView.isVisible = true + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + viewModel.handle(RoomMemberListAction.FilterMemberList(newText)) + return true + } + }) + } + override fun onDestroyView() { recyclerView.cleanup() super.onDestroyView() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index fedf1729ad..c45dbb1093 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -188,6 +188,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState override fun handle(action: RoomMemberListAction) { when (action) { is RoomMemberListAction.RevokeThreePidInvite -> handleRevokeThreePidInvite(action) + is RoomMemberListAction.FilterMemberList -> handleFilterMemberList(action) }.exhaustive } @@ -201,4 +202,28 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState ) } } + + private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) = withState { state -> + if (action.searchTerm.isBlank()) { + setState { copy(filteredRoomMemberSummaries = null) } + return@withState + } + val roomMemberSummaries = state.roomMemberSummaries.invoke() + roomMemberSummaries + ?.mapNotNull { (powerLevelCategory, roomMemberList) -> + roomMemberList + .filter { it.displayName?.contains(action.searchTerm).orFalse() || it.userId.contains(action.searchTerm) } + .takeIf { it.isNotEmpty() } + ?.let { filteredMemberList -> + Pair(powerLevelCategory, filteredMemberList) + } + } + ?.also { filteredRoomMemberSummaries -> + setState { + copy( + filteredRoomMemberSummaries = filteredRoomMemberSummaries + ) + } + } + } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 4d21aa103f..01fd290e78 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -31,6 +31,7 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async<RoomSummary> = Uninitialized, val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized, + val filteredRoomMemberSummaries: RoomMemberSummaries? = null, val threePidInvites: Async<List<Event>> = Uninitialized, val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index aa86ee342b..cdbf678b2b 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -53,6 +53,18 @@ </androidx.appcompat.widget.Toolbar> + <androidx.appcompat.widget.SearchView + android:id="@+id/searchView" + style="@style/VectorSearchView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:visibility="gone" + tools:visibility="visible" + app:queryHint="@string/search_hint" + app:layout_constraintTop_toBottomOf="@id/roomSettingsToolbar" + android:background="@null" /> + <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" android:layout_width="0dp" @@ -61,7 +73,7 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar" + app:layout_constraintTop_toBottomOf="@+id/searchView" tools:listitem="@layout/item_profile_action" /> <include layout="@layout/merge_overlay_waiting_view" /> From 78ed184f60381c529b287bea20f3a33975ccb661 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 21:40:10 +0200 Subject: [PATCH 02/17] Filter in the controller, to keep a live members list --- .../members/RoomMemberListController.kt | 24 +++++++----- .../members/RoomMemberListViewModel.kt | 26 +++---------- .../members/RoomMemberListViewState.kt | 2 +- .../members/RoomMemberSummaryFilter.kt | 37 +++++++++++++++++++ 4 files changed, 57 insertions(+), 32 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index f0c7e146c6..0d4b732da9 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -17,12 +17,6 @@ package im.vector.app.features.roomprofile.members import com.airbnb.epoxy.TypedEpoxyController -import org.matrix.android.sdk.api.session.events.model.Event -import org.matrix.android.sdk.api.session.events.model.toModel -import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent -import org.matrix.android.sdk.api.util.MatrixItem -import org.matrix.android.sdk.api.util.toMatrixItem import im.vector.app.R import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.profiles.buildProfileSection @@ -31,11 +25,18 @@ import im.vector.app.core.extensions.join import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.features.home.AvatarRenderer +import org.matrix.android.sdk.api.session.events.model.Event +import org.matrix.android.sdk.api.session.events.model.toModel +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomMemberListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, + private val roomMemberSummaryFilter: RoomMemberSummaryFilter, colorProvider: ColorProvider ) : TypedEpoxyController<RoomMemberListViewState>() { @@ -55,17 +56,20 @@ class RoomMemberListController @Inject constructor( override fun buildModels(data: RoomMemberListViewState?) { data ?: return - val roomMembersByPowerLevel = data.filteredRoomMemberSummaries ?: data.roomMemberSummaries.invoke() ?: return + roomMemberSummaryFilter.filter = data.filter + + val roomMembersByPowerLevel = data.roomMemberSummaries.invoke() ?: return val threePidInvites = data.threePidInvites().orEmpty() var threePidInvitesDone = threePidInvites.isEmpty() for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) { - if (roomMemberList.isEmpty()) { + val filteredRoomMemberList = roomMemberList.filter { roomMemberSummaryFilter.test(it) } + if (filteredRoomMemberList.isEmpty()) { continue } if (powerLevelCategory == RoomMemberListCategories.USER && !threePidInvitesDone) { - // If there is not regular invite, display threepid invite before the regular user + // If there is no regular invite, display threepid invite before the regular user buildProfileSection( stringProvider.getString(RoomMemberListCategories.INVITE.titleRes) ) @@ -77,7 +81,7 @@ class RoomMemberListController @Inject constructor( buildProfileSection( stringProvider.getString(powerLevelCategory.titleRes) ) - roomMemberList.join( + filteredRoomMemberList.join( each = { _, roomMember -> profileMatrixItem { id(roomMember.userId) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index c45dbb1093..60cb5867a7 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -203,27 +203,11 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState } } - private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) = withState { state -> - if (action.searchTerm.isBlank()) { - setState { copy(filteredRoomMemberSummaries = null) } - return@withState + private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) = withState { + setState { + copy( + filter = action.searchTerm + ) } - val roomMemberSummaries = state.roomMemberSummaries.invoke() - roomMemberSummaries - ?.mapNotNull { (powerLevelCategory, roomMemberList) -> - roomMemberList - .filter { it.displayName?.contains(action.searchTerm).orFalse() || it.userId.contains(action.searchTerm) } - .takeIf { it.isNotEmpty() } - ?.let { filteredMemberList -> - Pair(powerLevelCategory, filteredMemberList) - } - } - ?.also { filteredRoomMemberSummaries -> - setState { - copy( - filteredRoomMemberSummaries = filteredRoomMemberSummaries - ) - } - } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt index 01fd290e78..787ae920e1 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewState.kt @@ -31,7 +31,7 @@ data class RoomMemberListViewState( val roomId: String, val roomSummary: Async<RoomSummary> = Uninitialized, val roomMemberSummaries: Async<RoomMemberSummaries> = Uninitialized, - val filteredRoomMemberSummaries: RoomMemberSummaries? = null, + val filter: String = "", val threePidInvites: Async<List<Event>> = Uninitialized, val trustLevelMap: Async<Map<String, RoomEncryptionTrustLevel?>> = Uninitialized, val actionsPermissions: ActionPermissions = ActionPermissions() diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt new file mode 100644 index 0000000000..813679fef3 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2020 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.roomprofile.members + +import io.reactivex.functions.Predicate +import org.matrix.android.sdk.api.extensions.orFalse +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import javax.inject.Inject + +class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummary> { + var filter: String = "" + + override fun test(roomMemberSummary: RoomMemberSummary): Boolean { + if (filter.isBlank()) { + // No filter + return true + } + + return roomMemberSummary.displayName?.contains(filter, ignoreCase = true).orFalse() + // We should maybe exclude the domain from the userId + || roomMemberSummary.userId.contains(filter, ignoreCase = true) + } +} From 9df3de2afb33fcf3e8adcd75157e24d3faa98e6c Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 21:50:01 +0200 Subject: [PATCH 03/17] Rename method --- .../features/roomprofile/members/RoomMemberListController.kt | 4 ++-- .../features/roomprofile/members/RoomMemberListFragment.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index 0d4b732da9..ee6b9a71bf 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -42,7 +42,7 @@ class RoomMemberListController @Inject constructor( interface Callback { fun onRoomMemberClicked(roomMember: RoomMemberSummary) - fun onThreePidInvites(event: Event) + fun onThreePidInviteClicked(event: Event) } private val dividerColor = colorProvider.getColorFromAttribute(R.attr.vctr_list_divider_color) @@ -134,7 +134,7 @@ class RoomMemberListController @Inject constructor( avatarRenderer(avatarRenderer) editable(data.actionsPermissions.canRevokeThreePidInvite) clickListener { _ -> - callback?.onThreePidInvites(event) + callback?.onThreePidInviteClicked(event) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index e12cce9484..5367788466 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -109,7 +109,7 @@ class RoomMemberListFragment @Inject constructor( navigator.openRoomMemberProfile(roomMember.userId, roomId = roomProfileArgs.roomId, context = requireActivity()) } - override fun onThreePidInvites(event: Event) { + override fun onThreePidInviteClicked(event: Event) { // Display a dialog to revoke invite if power level is high enough val content = event.content.toModel<RoomThirdPartyInviteContent>() ?: return val stateKey = event.stateKey ?: return From f5580212ea51e1d961ab023aa3253ca8439211c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 22:16:13 +0200 Subject: [PATCH 04/17] Also filter threePids --- .../roomprofile/members/RoomMemberListController.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt index ee6b9a71bf..71ac7fcec4 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListController.kt @@ -59,7 +59,14 @@ class RoomMemberListController @Inject constructor( roomMemberSummaryFilter.filter = data.filter val roomMembersByPowerLevel = data.roomMemberSummaries.invoke() ?: return - val threePidInvites = data.threePidInvites().orEmpty() + val threePidInvites = data.threePidInvites() + ?.filter { event -> + event.content.toModel<RoomThirdPartyInviteContent>() + ?.takeIf { + data.filter.isEmpty() || it.displayName.contains(data.filter, ignoreCase = true) + } != null + } + .orEmpty() var threePidInvitesDone = threePidInvites.isEmpty() for ((powerLevelCategory, roomMemberList) in roomMembersByPowerLevel) { @@ -100,12 +107,13 @@ class RoomMemberListController @Inject constructor( } } ) - if (powerLevelCategory == RoomMemberListCategories.INVITE) { + if (powerLevelCategory == RoomMemberListCategories.INVITE && !threePidInvitesDone) { // Display the threepid invite after the regular invite dividerItem { id("divider_threepidinvites") color(dividerColor) } + buildThreePidInvites(data) threePidInvitesDone = true } From 50f6a4732c40b89622e3ba28a194ef71f242124b Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 22:21:13 +0200 Subject: [PATCH 05/17] Change hint --- .../features/roomprofile/members/RoomMemberListFragment.kt | 1 + .../src/main/res/layout/fragment_room_setting_generic.xml | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index 5367788466..e4d17b4da6 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -83,6 +83,7 @@ class RoomMemberListFragment @Inject constructor( private fun setupSearchView() { searchView.isVisible = true + searchView.queryHint = getString(R.string.search_members_hint) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { return true diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index cdbf678b2b..03e211a806 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -59,11 +59,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" + android:background="@null" android:visibility="gone" - tools:visibility="visible" - app:queryHint="@string/search_hint" app:layout_constraintTop_toBottomOf="@id/roomSettingsToolbar" - android:background="@null" /> + tools:queryHint="@string/search_hint" + tools:visibility="visible" /> <androidx.recyclerview.widget.RecyclerView android:id="@+id/recyclerView" From 6147a87e46717489daa2a050aa94a84d3238e8f3 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 23:06:59 +0200 Subject: [PATCH 06/17] Hide the SearchView when scrolling down --- .../members/RoomMemberListFragment.kt | 2 +- .../layout/fragment_room_setting_generic.xml | 52 +++++++++++++------ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt index e4d17b4da6..67577e866e 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListFragment.kt @@ -82,7 +82,7 @@ class RoomMemberListFragment @Inject constructor( } private fun setupSearchView() { - searchView.isVisible = true + searchViewAppBarLayout.isVisible = true searchView.queryHint = getString(R.string.search_members_hint) searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index 03e211a806..4ab6d2c7df 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -53,28 +53,46 @@ </androidx.appcompat.widget.Toolbar> - <androidx.appcompat.widget.SearchView - android:id="@+id/searchView" - style="@style/VectorSearchView" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginTop="16dp" - android:background="@null" - android:visibility="gone" - app:layout_constraintTop_toBottomOf="@id/roomSettingsToolbar" - tools:queryHint="@string/search_hint" - tools:visibility="visible" /> - - <androidx.recyclerview.widget.RecyclerView - android:id="@+id/recyclerView" + <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="0dp" android:layout_height="0dp" - android:overScrollMode="always" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/searchView" - tools:listitem="@layout/item_profile_action" /> + app:layout_constraintTop_toBottomOf="@+id/roomSettingsToolbar"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:overScrollMode="always" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:listitem="@layout/item_profile_action" /> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/searchViewAppBarLayout" + style="@style/VectorAppBarLayoutStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="4dp" + android:visibility="gone" + tools:visibility="visible"> + + <androidx.appcompat.widget.SearchView + android:id="@+id/searchView" + style="@style/VectorSearchView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:background="@null" + android:minHeight="0dp" + app:layout_scrollFlags="scroll|enterAlways|snap" + tools:queryHint="@string/search_hint" /> + + </com.google.android.material.appbar.AppBarLayout> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> <include layout="@layout/merge_overlay_waiting_view" /> From c0842d4da70e56eeb1fa20ec6d6661a09e0c6497 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 23:20:28 +0200 Subject: [PATCH 07/17] Also apply the filter to banned user screen --- CHANGES.md | 2 +- .../banned/RoomBannedListMemberAction.kt | 1 + .../banned/RoomBannedListMemberViewModel.kt | 10 ++++ .../banned/RoomBannedMemberListController.kt | 59 ++++++++++--------- .../banned/RoomBannedMemberListFragment.kt | 18 ++++++ .../banned/RoomBannedMemberListViewState.kt | 1 + vector/src/main/res/values/strings.xml | 1 + 7 files changed, 64 insertions(+), 28 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f5d6fca073..9103acaa60 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,7 @@ Improvements 🙌: - Small optimisation of scrolling experience in timeline (#2114) - Allow user to reset cross signing if he has no way to recover (#2052) - Create home shortcut for any room (#1525) - - Filter room member by name (#2184) + - Filter room member (and banned users) by name (#2184) Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt index 9af554e883..67cacf4541 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberAction.kt @@ -22,4 +22,5 @@ import im.vector.app.core.platform.VectorViewModelAction sealed class RoomBannedListMemberAction : VectorViewModelAction { data class QueryInfo(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() data class UnBanUser(val roomMemberSummary: RoomMemberSummary) : RoomBannedListMemberAction() + data class Filter(val filter: String) : RoomBannedListMemberAction() } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt index b65b3de807..d1acd80c25 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedListMemberViewModel.kt @@ -35,6 +35,7 @@ import org.matrix.android.sdk.internal.util.awaitCallback import org.matrix.android.sdk.rx.rx import org.matrix.android.sdk.rx.unwrap import im.vector.app.R +import im.vector.app.core.extensions.exhaustive import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.resources.StringProvider import im.vector.app.features.powerlevel.PowerLevelsObservableFactory @@ -90,6 +91,15 @@ class RoomBannedListMemberViewModel @AssistedInject constructor(@Assisted initia when (action) { is RoomBannedListMemberAction.QueryInfo -> onQueryBanInfo(action.roomMemberSummary) is RoomBannedListMemberAction.UnBanUser -> unBanUser(action.roomMemberSummary) + is RoomBannedListMemberAction.Filter -> handleFilter(action) + }.exhaustive + } + + private fun handleFilter(action: RoomBannedListMemberAction.Filter) { + setState { + copy( + filter = action.filter + ) } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt index a65a30441d..2a0c787a7a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListController.kt @@ -17,8 +17,6 @@ package im.vector.app.features.roomprofile.banned import com.airbnb.epoxy.TypedEpoxyController -import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary -import org.matrix.android.sdk.api.util.toMatrixItem import im.vector.app.R import im.vector.app.core.epoxy.dividerItem import im.vector.app.core.epoxy.profiles.buildProfileSection @@ -28,11 +26,15 @@ import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.StringProvider import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.features.home.AvatarRenderer +import im.vector.app.features.roomprofile.members.RoomMemberSummaryFilter +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.util.toMatrixItem import javax.inject.Inject class RoomBannedMemberListController @Inject constructor( private val avatarRenderer: AvatarRenderer, private val stringProvider: StringProvider, + private val roomMemberSummaryFilter: RoomMemberSummaryFilter, colorProvider: ColorProvider ) : TypedEpoxyController<RoomBannedMemberListViewState>() { @@ -63,34 +65,37 @@ class RoomBannedMemberListController @Inject constructor( } else { buildProfileSection(quantityString) - bannedList.join( - each = { _, roomMember -> - val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId) - profileMatrixItemWithProgress { - id(roomMember.userId) - matrixItem(roomMember.toMatrixItem()) - avatarRenderer(avatarRenderer) - apply { - if (actionInProgress) { - inProgress(true) - editable(false) - } else { - inProgress(false) - editable(true) - clickListener { _ -> - callback?.onUnbanClicked(roomMember) + roomMemberSummaryFilter.filter = data.filter + bannedList + .filter { roomMemberSummaryFilter.test(it) } + .join( + each = { _, roomMember -> + val actionInProgress = data.onGoingModerationAction.contains(roomMember.userId) + profileMatrixItemWithProgress { + id(roomMember.userId) + matrixItem(roomMember.toMatrixItem()) + avatarRenderer(avatarRenderer) + apply { + if (actionInProgress) { + inProgress(true) + editable(false) + } else { + inProgress(false) + editable(true) + clickListener { _ -> + callback?.onUnbanClicked(roomMember) + } + } } } + }, + between = { _, roomMemberBefore -> + dividerItem { + id("divider_${roomMemberBefore.userId}") + color(dividerColor) + } } - } - }, - between = { _, roomMemberBefore -> - dividerItem { - id("divider_${roomMemberBefore.userId}") - color(dividerColor) - } - } - ) + ) } } } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt index 480deea6af..84eccccfae 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListFragment.kt @@ -19,6 +19,8 @@ package im.vector.app.features.roomprofile.banned import android.os.Bundle import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.SearchView +import androidx.core.view.isVisible import com.airbnb.mvrx.args import com.airbnb.mvrx.fragmentViewModel import com.airbnb.mvrx.withState @@ -53,6 +55,7 @@ class RoomBannedMemberListFragment @Inject constructor( super.onViewCreated(view, savedInstanceState) roomMemberListController.callback = this setupToolbar(roomSettingsToolbar) + setupSearchView() recyclerView.configureWith(roomMemberListController, hasFixedSize = true) viewModel.observeViewEvents { @@ -84,6 +87,21 @@ class RoomBannedMemberListFragment @Inject constructor( super.onDestroyView() } + private fun setupSearchView() { + searchViewAppBarLayout.isVisible = true + searchView.queryHint = getString(R.string.search_banned_user_hint) + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String): Boolean { + return true + } + + override fun onQueryTextChange(newText: String): Boolean { + viewModel.handle(RoomBannedListMemberAction.Filter(newText)) + return true + } + }) + } + override fun invalidate() = withState(viewModel) { viewState -> roomMemberListController.setData(viewState) renderRoomSummary(viewState) diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt index ee8e534b1f..6d8bb2f42a 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/banned/RoomBannedMemberListViewState.kt @@ -27,6 +27,7 @@ data class RoomBannedMemberListViewState( val roomId: String, val roomSummary: Async<RoomSummary> = Uninitialized, val bannedMemberSummaries: Async<List<RoomMemberSummary>> = Uninitialized, + val filter: String = "", val onGoingModerationAction: List<String> = emptyList(), val canUserBan: Boolean = false ) : MvRxState { diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 9310af7577..9099c276e8 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -623,6 +623,7 @@ <!-- Search --> <string name="search_hint">Search</string> <string name="search_members_hint">Filter room members</string> + <string name="search_banned_user_hint">Filter banned users</string> <string name="search_no_results">No results</string> <string name="tab_title_search_rooms">ROOMS</string> <string name="tab_title_search_messages">MESSAGES</string> From 8170f523e091ef4ca366e4e787a79fb9fa467a12 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Fri, 2 Oct 2020 23:29:48 +0200 Subject: [PATCH 08/17] Small improvement --- .../im/vector/app/features/home/room/list/RoomListNameFilter.kt | 2 +- .../app/features/roomprofile/members/RoomMemberListViewModel.kt | 2 +- .../app/features/roomprofile/members/RoomMemberSummaryFilter.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt index 4c4a9755d2..6e787b8b95 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/list/RoomListNameFilter.kt @@ -25,7 +25,7 @@ class RoomListNameFilter @Inject constructor() : Predicate<RoomSummary> { var filter: String = "" override fun test(roomSummary: RoomSummary): Boolean { - if (filter.isBlank()) { + if (filter.isEmpty()) { // No filter return true } diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt index 60cb5867a7..9e402c675b 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberListViewModel.kt @@ -203,7 +203,7 @@ class RoomMemberListViewModel @AssistedInject constructor(@Assisted initialState } } - private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) = withState { + private fun handleFilterMemberList(action: RoomMemberListAction.FilterMemberList) { setState { copy( filter = action.searchTerm diff --git a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt index 813679fef3..e2cc3f7b99 100644 --- a/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt +++ b/vector/src/main/java/im/vector/app/features/roomprofile/members/RoomMemberSummaryFilter.kt @@ -25,7 +25,7 @@ class RoomMemberSummaryFilter @Inject constructor() : Predicate<RoomMemberSummar var filter: String = "" override fun test(roomMemberSummary: RoomMemberSummary): Boolean { - if (filter.isBlank()) { + if (filter.isEmpty()) { // No filter return true } From 1b3a5097c10cd431840b2c7a40970ae32b399fef Mon Sep 17 00:00:00 2001 From: Cadence Ember <cloudrac3r@vivaldi.net> Date: Sun, 4 Oct 2020 12:43:26 +1300 Subject: [PATCH 09/17] Replace "him" with "them" in report interface All genders can spam a chatroom :) --- vector/src/main/res/values/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml index 9310af7577..c025054f98 100644 --- a/vector/src/main/res/values/strings.xml +++ b/vector/src/main/res/values/strings.xml @@ -1851,11 +1851,11 @@ <string name="block_user">"IGNORE USER"</string> <string name="content_reported_title">"Content reported"</string> - <string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string> + <string name="content_reported_content">"This content was reported.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string> <string name="content_reported_as_spam_title">"Reported as spam"</string> - <string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string> + <string name="content_reported_as_spam_content">"This content was reported as spam.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string> <string name="content_reported_as_inappropriate_title">"Reported as inappropriate"</string> - <string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can block him to hide his messages"</string> + <string name="content_reported_as_inappropriate_content">"This content was reported as inappropriate.\n\nIf you don't want to see any more content from this user, you can ignore them to hide their messages."</string> <string name="permissions_rationale_msg_keys_backup_export">Element needs permission to save your E2E keys on disk.\n\nPlease allow access on the next pop-up to be able to export your keys manually.</string> From b2030930739b839b8d5dc483d05e95537b7f2ba0 Mon Sep 17 00:00:00 2001 From: Benoit Marty <benoit@matrix.org> Date: Sun, 4 Oct 2020 11:17:57 +0200 Subject: [PATCH 10/17] Use an extra container for the margin to be taken into account by the layout_scrollFlags --- .../layout/fragment_room_setting_generic.xml | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/vector/src/main/res/layout/fragment_room_setting_generic.xml b/vector/src/main/res/layout/fragment_room_setting_generic.xml index 4ab6d2c7df..07744436ea 100644 --- a/vector/src/main/res/layout/fragment_room_setting_generic.xml +++ b/vector/src/main/res/layout/fragment_room_setting_generic.xml @@ -78,17 +78,24 @@ android:visibility="gone" tools:visibility="visible"> - <androidx.appcompat.widget.SearchView - android:id="@+id/searchView" - style="@style/VectorSearchView" + <!-- Use an extra container for the margin to be taken into account by the layout_scrollFlags --> + <FrameLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" - android:layout_marginBottom="4dp" - android:background="@null" - android:minHeight="0dp" - app:layout_scrollFlags="scroll|enterAlways|snap" - tools:queryHint="@string/search_hint" /> + app:layout_scrollFlags="scroll|enterAlways|snap"> + + <androidx.appcompat.widget.SearchView + android:id="@+id/searchView" + style="@style/VectorSearchView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:background="@null" + android:minHeight="0dp" + tools:queryHint="@string/search_hint" /> + + </FrameLayout> </com.google.android.material.appbar.AppBarLayout> From aa0520d47dba1c2b0d50b946aef4784a6317d75f Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.orgfrancoisg@matrix.orgfrancoisg@matrix.orgfrancoisg@matrix.org> Date: Tue, 22 Sep 2020 18:56:05 +0200 Subject: [PATCH 11/17] Start reworking draft (simplify) --- .../java/org/matrix/android/sdk/rx/RxRoom.kt | 7 +- .../sdk/api/session/room/send/DraftService.kt | 11 +- .../session/room/draft/DefaultDraftService.kt | 7 +- .../session/room/draft/DraftRepository.kt | 111 ++++++------------ .../home/room/detail/RoomDetailViewModel.kt | 61 ++-------- 5 files changed, 65 insertions(+), 132 deletions(-) diff --git a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt index 0717a10d03..2bb6c0ff69 100644 --- a/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt +++ b/matrix-sdk-android-rx/src/main/java/org/matrix/android/sdk/rx/RxRoom.kt @@ -101,8 +101,11 @@ class RxRoom(private val room: Room) { return room.getEventReadReceiptsLive(eventId).asObservable() } - fun liveDrafts(): Observable<List<UserDraft>> { - return room.getDraftsLive().asObservable() + fun liveDraft(): Observable<Optional<UserDraft>> { + return room.getDraftLive().asObservable() + .startWithCallable { + room.getDraft().toOptional() + } } fun liveNotificationState(): Observable<RoomNotificationState> { diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt index dc91d5177f..6ef73d6486 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/DraftService.kt @@ -20,6 +20,7 @@ package org.matrix.android.sdk.api.session.room.send import androidx.lifecycle.LiveData import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional interface DraftService { @@ -34,8 +35,12 @@ interface DraftService { fun deleteDraft(callback: MatrixCallback<Unit>): Cancelable /** - * Return the current drafts if any, as a live data - * The draft list can contain one draft for {regular, reply, quote} and an arbitrary number of {edit} drafts + * Return the current draft or null */ - fun getDraftsLive(): LiveData<List<UserDraft>> + fun getDraft(): UserDraft? + + /** + * Return the current draft if any, as a live data + */ + fun getDraftLive(): LiveData<Optional<UserDraft>> } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt index dafa7df0eb..0426ee13e1 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DefaultDraftService.kt @@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.session.room.send.DraftService import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.util.Cancelable +import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.internal.task.TaskExecutor import org.matrix.android.sdk.internal.task.launchToCallback import org.matrix.android.sdk.internal.util.MatrixCoroutineDispatchers @@ -55,7 +56,11 @@ internal class DefaultDraftService @AssistedInject constructor(@Assisted private } } - override fun getDraftsLive(): LiveData<List<UserDraft>> { + override fun getDraft(): UserDraft? { + return draftRepository.getDraft(roomId) + } + + override fun getDraftLive(): LiveData<Optional<UserDraft>> { return draftRepository.getDraftsLive(roomId) } } diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt index bc50b2d990..9262eb2f49 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt @@ -20,35 +20,61 @@ package org.matrix.android.sdk.internal.session.room.draft import androidx.lifecycle.LiveData import androidx.lifecycle.Transformations import com.zhuinden.monarchy.Monarchy +import io.realm.Realm +import io.realm.kotlin.createObject import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.room.send.UserDraft +import org.matrix.android.sdk.api.util.Optional +import org.matrix.android.sdk.api.util.toOptional import org.matrix.android.sdk.internal.database.mapper.DraftMapper -import org.matrix.android.sdk.internal.database.model.DraftEntity import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.UserDraftsEntity import org.matrix.android.sdk.internal.database.query.where import org.matrix.android.sdk.internal.di.SessionDatabase import org.matrix.android.sdk.internal.util.awaitTransaction -import io.realm.Realm -import io.realm.kotlin.createObject import timber.log.Timber import javax.inject.Inject -class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { +internal class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { suspend fun saveDraft(roomId: String, userDraft: UserDraft) { monarchy.awaitTransaction { - saveDraft(it, userDraft, roomId) + saveDraftInDb(it, userDraft, roomId) } } suspend fun deleteDraft(roomId: String) { monarchy.awaitTransaction { - deleteDraft(it, roomId) + deleteDraftFromDb(it, roomId) } } - private fun deleteDraft(realm: Realm, roomId: String) { + fun getDraft(roomId: String): UserDraft? { + return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + UserDraftsEntity.where(realm, roomId).findFirst() + ?.userDrafts + ?.firstOrNull() + ?.let { + DraftMapper.map(it) + } + } + } + + fun getDraftsLive(roomId: String): LiveData<Optional<UserDraft>> { + val liveData = monarchy.findAllMappedWithChanges( + { UserDraftsEntity.where(it, roomId) }, + { + it.userDrafts.map { draft -> + DraftMapper.map(draft) + } + } + ) + return Transformations.map(liveData) { + it.firstOrNull()?.firstOrNull().toOptional() + } + } + + private fun deleteDraftFromDb(realm: Realm, roomId: String) { UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> if (userDraftsEntity.userDrafts.isNotEmpty()) { userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) @@ -56,7 +82,7 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: } } - private fun saveDraft(realm: Realm, draft: UserDraft, roomId: String) { + private fun saveDraftInDb(realm: Realm, draft: UserDraft, roomId: String) { val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() ?: realm.createObject(roomId) @@ -68,62 +94,15 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: userDraftsEntity.let { userDraftEntity -> // Save only valid draft if (draft.isValid()) { - // Add a new draft or update the current one? + // Replace the current draft val newDraft = DraftMapper.map(draft) - - // Is it an update of the top draft? - val topDraft = userDraftEntity.userDrafts.lastOrNull() - - if (topDraft == null) { - Timber.d("Draft: create a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else if (topDraft.draftMode == DraftEntity.MODE_EDIT) { - // top draft is an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - if (topDraft.linkedEventId == newDraft.linkedEventId) { - // Update the top draft - Timber.d("Draft: update the top edit draft ${privacySafe(draft)}") - topDraft.content = newDraft.content - } else { - // Check a previously EDIT draft with the same id - val existingEditDraftOfSameEvent = userDraftEntity.userDrafts.find { - it.draftMode == DraftEntity.MODE_EDIT && it.linkedEventId == newDraft.linkedEventId - } - - if (existingEditDraftOfSameEvent != null) { - // Ignore the new text, restore what was typed before, by putting the draft to the top - Timber.d("Draft: restore a previously edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.remove(existingEditDraftOfSameEvent) - userDraftEntity.userDrafts.add(existingEditDraftOfSameEvent) - } else { - Timber.d("Draft: add a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } - } else { - // Add a new regular draft to the top - Timber.d("Draft: add a new draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } - } else { - // Top draft is not an edit - if (newDraft.draftMode == DraftEntity.MODE_EDIT) { - Timber.d("Draft: create a new edit draft ${privacySafe(draft)}") - userDraftEntity.userDrafts.add(newDraft) - } else { - // Update the top draft - Timber.d("Draft: update the top draft ${privacySafe(draft)}") - topDraft.draftMode = newDraft.draftMode - topDraft.content = newDraft.content - topDraft.linkedEventId = newDraft.linkedEventId - } - } + Timber.d("Draft: create a new draft ${privacySafe(draft)}") + userDraftEntity.userDrafts.clear() + userDraftEntity.userDrafts.add(newDraft) } else { // There is no draft to save, so the composer was clear Timber.d("Draft: delete a draft") - val topDraft = userDraftEntity.userDrafts.lastOrNull() - if (topDraft == null) { Timber.d("Draft: nothing to do") } else { @@ -135,20 +114,6 @@ class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: } } - fun getDraftsLive(roomId: String): LiveData<List<UserDraft>> { - val liveData = monarchy.findAllMappedWithChanges( - { UserDraftsEntity.where(it, roomId) }, - { - it.userDrafts.map { draft -> - DraftMapper.map(draft) - } - } - ) - return Transformations.map(liveData) { - it.firstOrNull().orEmpty() - } - } - private fun privacySafe(o: Any): Any { if (BuildConfig.LOG_PRIVATE_DATA) { return o diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 1b5e928843..42ce8567cd 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -680,7 +680,7 @@ class RoomDetailViewModel @AssistedInject constructor( } }.exhaustive } - is SendMode.EDIT -> { + is SendMode.EDIT -> { // is original event a reply? val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId ?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId @@ -706,7 +706,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.QUOTE -> { + is SendMode.QUOTE -> { val messageContent: MessageContent? = state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel() ?: state.sendMode.timelineEvent.root.getClearContent().toModel() @@ -729,7 +729,7 @@ class RoomDetailViewModel @AssistedInject constructor( _viewEvents.post(RoomDetailViewEvents.MessageSent) popDraft() } - is SendMode.REPLY -> { + is SendMode.REPLY -> { state.sendMode.timelineEvent.let { room.replyToMessage(it, action.text.toString(), action.autoMarkdown) _viewEvents.post(RoomDetailViewEvents.MessageSent) @@ -741,6 +741,9 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun popDraft() { + setState { + copy(sendMode = SendMode.REGULAR("")) + } room.deleteDraft(NoOpMatrixCallback()) } @@ -915,73 +918,25 @@ class RoomDetailViewModel @AssistedInject constructor( } private fun handleEditAction(action: RoomDetailAction.EnterEditMode) { - saveCurrentDraft(action.text) - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> - setState { copy(sendMode = SendMode.EDIT(timelineEvent, action.text)) } - timelineEvent.root.eventId?.let { - room.saveDraft(UserDraft.EDIT(it, timelineEvent.getTextEditableContent() ?: ""), NoOpMatrixCallback()) - } + setState { copy(sendMode = SendMode.EDIT(timelineEvent, timelineEvent.getTextEditableContent() ?: "")) } } } private fun handleQuoteAction(action: RoomDetailAction.EnterQuoteMode) { - saveCurrentDraft(action.text) - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.QUOTE(timelineEvent, action.text)) } - withState { state -> - // Save a new draft and keep the previously entered text, if it was not an edit - timelineEvent.root.eventId?.let { - if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.QUOTE(it, ""), NoOpMatrixCallback()) - } else { - room.saveDraft(UserDraft.QUOTE(it, action.text), NoOpMatrixCallback()) - } - } - } } } private fun handleReplyAction(action: RoomDetailAction.EnterReplyMode) { - saveCurrentDraft(action.text) - room.getTimeLineEvent(action.eventId)?.let { timelineEvent -> setState { copy(sendMode = SendMode.REPLY(timelineEvent, action.text)) } - withState { state -> - // Save a new draft and keep the previously entered text, if it was not an edit - timelineEvent.root.eventId?.let { - if (state.sendMode is SendMode.EDIT) { - room.saveDraft(UserDraft.REPLY(it, ""), NoOpMatrixCallback()) - } else { - room.saveDraft(UserDraft.REPLY(it, action.text), NoOpMatrixCallback()) - } - } - } - } - } - - private fun saveCurrentDraft(draft: String) { - // Save the draft with the current text if any - withState { - if (draft.isNotBlank()) { - when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(draft), NoOpMatrixCallback()) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, draft), NoOpMatrixCallback()) - } - } } } private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState { - if (it.sendMode is SendMode.EDIT) { - room.deleteDraft(NoOpMatrixCallback()) - } else { - // Save a new draft and keep the previously entered text - room.saveDraft(UserDraft.REGULAR(action.text), NoOpMatrixCallback()) - } + room.deleteDraft(NoOpMatrixCallback()) setState { copy(sendMode = SendMode.REGULAR(action.text)) } } From 91b81af5a8d74a3b114eb99c6cc12d1be5908cb7 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.org> Date: Wed, 23 Sep 2020 13:32:07 +0200 Subject: [PATCH 12/17] Draft: handle sharing so it doesn't destroy the previous draft --- .../session/room/draft/DraftRepository.kt | 6 +- .../home/room/detail/RoomDetailAction.kt | 2 +- .../home/room/detail/RoomDetailFragment.kt | 9 +- .../home/room/detail/RoomDetailViewModel.kt | 184 +++++++++--------- .../home/room/detail/RoomDetailViewState.kt | 9 +- 5 files changed, 106 insertions(+), 104 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt index 9262eb2f49..7ca5e3c3ce 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt @@ -75,11 +75,7 @@ internal class DraftRepository @Inject constructor(@SessionDatabase private val } private fun deleteDraftFromDb(realm: Realm, roomId: String) { - UserDraftsEntity.where(realm, roomId).findFirst()?.let { userDraftsEntity -> - if (userDraftsEntity.userDrafts.isNotEmpty()) { - userDraftsEntity.userDrafts.removeAt(userDraftsEntity.userDrafts.size - 1) - } - } + UserDraftsEntity.where(realm, roomId).findFirst()?.userDrafts?.clear() } private fun saveDraftInDb(realm: Realm, draft: UserDraft, roomId: String) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 4bdf2e7e57..5f851b8da7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -51,7 +51,7 @@ sealed class RoomDetailAction : VectorViewModelAction { data class EnterEditMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterQuoteMode(val eventId: String, val text: String) : RoomDetailAction() data class EnterReplyMode(val eventId: String, val text: String) : RoomDetailAction() - data class ExitSpecialMode(val text: String) : RoomDetailAction() + data class EnterRegularMode(val text: String, val fromSharing: Boolean) : RoomDetailAction() data class ResendMessage(val eventId: String) : RoomDetailAction() data class RemoveFailedEcho(val eventId: String) : RoomDetailAction() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt index 51aeda2aab..2fc2fe94a5 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt @@ -485,8 +485,7 @@ class RoomDetailFragment @Inject constructor( if (savedInstanceState == null) { when (val sharedData = roomDetailArgs.sharedData) { is SharedData.Text -> { - // Save a draft to set the shared text to the composer - roomDetailViewModel.handle(RoomDetailAction.SaveDraft(sharedData.text)) + roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(sharedData.text, fromSharing = true)) } is SharedData.Attachments -> { // open share edition @@ -1014,7 +1013,7 @@ class RoomDetailFragment @Inject constructor( } override fun onCloseRelatedMessage() { - roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString())) + roomDetailViewModel.handle(RoomDetailAction.EnterRegularMode(composerLayout.text.toString(), false)) } override fun onRichContentSelected(contentUri: Uri): Boolean { @@ -1147,12 +1146,8 @@ class RoomDetailFragment @Inject constructor( private fun renderSendMessageResult(sendMessageResult: RoomDetailViewEvents.SendMessageResult) { when (sendMessageResult) { - is RoomDetailViewEvents.MessageSent -> { - updateComposerText("") - } is RoomDetailViewEvents.SlashCommandHandled -> { sendMessageResult.messageRes?.let { showSnackWithMessage(getString(it)) } - updateComposerText("") } is RoomDetailViewEvents.SlashCommandError -> { displayCommandError(getString(R.string.command_problem_with_parameters, sendMessageResult.command.command)) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 42ce8567cd..985604a49c 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -164,7 +164,7 @@ class RoomDetailViewModel @AssistedInject constructor( getUnreadState() observeSyncState() observeEventDisplayedActions() - observeDrafts() + getDraftIfAny() observeUnreadState() observeMyRoomMember() observeActiveRoomWidgets() @@ -226,52 +226,52 @@ class RoomDetailViewModel @AssistedInject constructor( override fun handle(action: RoomDetailAction) { when (action) { - is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) - is RoomDetailAction.SaveDraft -> handleSaveDraft(action) - is RoomDetailAction.SendMessage -> handleSendMessage(action) - is RoomDetailAction.SendMedia -> handleSendMedia(action) - is RoomDetailAction.SendSticker -> handleSendSticker(action) - is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) - is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) - is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) - is RoomDetailAction.SendReaction -> handleSendReaction(action) - is RoomDetailAction.AcceptInvite -> handleAcceptInvite() - is RoomDetailAction.RejectInvite -> handleRejectInvite() - is RoomDetailAction.RedactAction -> handleRedactEvent(action) - is RoomDetailAction.UndoReaction -> handleUndoReact(action) - is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) - is RoomDetailAction.ExitSpecialMode -> handleExitSpecialMode(action) - is RoomDetailAction.EnterEditMode -> handleEditAction(action) - is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) - is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) - is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) - is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) - is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) - is RoomDetailAction.ResendMessage -> handleResendEvent(action) - is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) - is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() - is RoomDetailAction.ResendAll -> handleResendAll() - is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() - is RoomDetailAction.ReportContent -> handleReportContent(action) - is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) + is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action) + is RoomDetailAction.SaveDraft -> handleSaveDraft(action) + is RoomDetailAction.SendMessage -> handleSendMessage(action) + is RoomDetailAction.SendMedia -> handleSendMedia(action) + is RoomDetailAction.SendSticker -> handleSendSticker(action) + is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action) + is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action) + is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action) + is RoomDetailAction.SendReaction -> handleSendReaction(action) + is RoomDetailAction.AcceptInvite -> handleAcceptInvite() + is RoomDetailAction.RejectInvite -> handleRejectInvite() + is RoomDetailAction.RedactAction -> handleRedactEvent(action) + is RoomDetailAction.UndoReaction -> handleUndoReact(action) + is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action) + is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action) + is RoomDetailAction.EnterEditMode -> handleEditAction(action) + is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action) + is RoomDetailAction.EnterReplyMode -> handleReplyAction(action) + is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action) + is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action) + is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action) + is RoomDetailAction.ResendMessage -> handleResendEvent(action) + is RoomDetailAction.RemoveFailedEcho -> handleRemove(action) + is RoomDetailAction.ClearSendQueue -> handleClearSendQueue() + is RoomDetailAction.ResendAll -> handleResendAll() + is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead() + is RoomDetailAction.ReportContent -> handleReportContent(action) + is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() - is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() - is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) - is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) - is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) - is RoomDetailAction.RequestVerification -> handleRequestVerification(action) - is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) - is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) - is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) - is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() - is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() - is RoomDetailAction.StartCall -> handleStartCall(action) - is RoomDetailAction.EndCall -> handleEndCall() - is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() - is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) - is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) - is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) - is RoomDetailAction.CancelSend -> handleCancel(action) + is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() + is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action) + is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action) + is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action) + is RoomDetailAction.RequestVerification -> handleRequestVerification(action) + is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action) + is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action) + is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action) + is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() + is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() + is RoomDetailAction.StartCall -> handleStartCall(action) + is RoomDetailAction.EndCall -> handleEndCall() + is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() + is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) + is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId) + is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action) + is RoomDetailAction.CancelSend -> handleCancel(action) }.exhaustive } @@ -451,45 +451,48 @@ class RoomDetailViewModel @AssistedInject constructor( */ private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { withState { - when (it.sendMode) { - is SendMode.REGULAR -> room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) - is SendMode.REPLY -> room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - is SendMode.QUOTE -> room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - is SendMode.EDIT -> room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - }.exhaustive + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.REPLY -> { + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.QUOTE -> { + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.EDIT -> { + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + } + } } } - private fun observeDrafts() { - room.rx().liveDrafts() - .subscribe { - Timber.d("Draft update --> SetState") - setState { - val draft = it.lastOrNull() ?: UserDraft.REGULAR("") - copy( - // Create a sendMode from a draft and retrieve the TimelineEvent - sendMode = when (draft) { - is UserDraft.REGULAR -> SendMode.REGULAR(draft.text) - is UserDraft.QUOTE -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.QUOTE(timelineEvent, draft.text) - } - } - is UserDraft.REPLY -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.REPLY(timelineEvent, draft.text) - } - } - is UserDraft.EDIT -> { - room.getTimeLineEvent(draft.linkedEventId)?.let { timelineEvent -> - SendMode.EDIT(timelineEvent, draft.text) - } - } - } ?: SendMode.REGULAR("") - ) - } - } - .disposeOnClear() + private fun getDraftIfAny() { + val currentDraft = room.getDraft() ?: return + setState { + copy( + // Create a sendMode from a draft and retrieve the TimelineEvent + sendMode = when (currentDraft) { + is UserDraft.REGULAR -> SendMode.REGULAR(currentDraft.text, false) + is UserDraft.QUOTE -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.QUOTE(timelineEvent, currentDraft.text) + } + } + is UserDraft.REPLY -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.REPLY(timelineEvent, currentDraft.text) + } + } + is UserDraft.EDIT -> { + room.getTimeLineEvent(currentDraft.linkedEventId)?.let { timelineEvent -> + SendMode.EDIT(timelineEvent, currentDraft.text) + } + } + } ?: SendMode.REGULAR("", fromSharing = false) + ) + } } private fun handleUserIsTyping(action: RoomDetailAction.UserIsTyping) { @@ -740,11 +743,15 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun popDraft() { - setState { - copy(sendMode = SendMode.REGULAR("")) + private fun popDraft() = withState { + if (it.sendMode is SendMode.REGULAR && it.sendMode.fromSharing) { + // If we were sharing, we want to get back our last value from draft + getDraftIfAny() + } else { + // Otherwise we clear the composer and remove the draft from db + setState { copy(sendMode = SendMode.REGULAR("", false)) } + room.deleteDraft(NoOpMatrixCallback()) } - room.deleteDraft(NoOpMatrixCallback()) } private fun handleJoinToAnotherRoomSlashCommand(command: ParsedCommand.JoinRoom) { @@ -935,9 +942,8 @@ class RoomDetailViewModel @AssistedInject constructor( } } - private fun handleExitSpecialMode(action: RoomDetailAction.ExitSpecialMode) = withState { - room.deleteDraft(NoOpMatrixCallback()) - setState { copy(sendMode = SendMode.REGULAR(action.text)) } + private fun handleEnterRegularMode(action: RoomDetailAction.EnterRegularMode) = setState { + copy(sendMode = SendMode.REGULAR(action.text, action.fromSharing)) } private fun handleOpenOrDownloadFile(action: RoomDetailAction.DownloadOrOpen) { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 36acc35148..c443d61252 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -37,7 +37,12 @@ import org.matrix.android.sdk.api.session.widgets.model.Widget * Depending on the state the bottom toolbar will change (icons/preview/actions...) */ sealed class SendMode(open val text: String) { - data class REGULAR(override val text: String) : SendMode(text) + data class REGULAR( + override val text: String, + val fromSharing: Boolean, + // This is necessary for forcing refresh on selectSubscribe + private val ts: Long = System.currentTimeMillis() + ) : SendMode(text) data class QUOTE(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class EDIT(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) data class REPLY(val timelineEvent: TimelineEvent, override val text: String) : SendMode(text) @@ -58,7 +63,7 @@ data class RoomDetailViewState( val asyncRoomSummary: Async<RoomSummary> = Uninitialized, val activeRoomWidgets: Async<List<Widget>> = Uninitialized, val typingMessage: String? = null, - val sendMode: SendMode = SendMode.REGULAR(""), + val sendMode: SendMode = SendMode.REGULAR("", false), val tombstoneEvent: Event? = null, val tombstoneEventHandling: Async<String> = Uninitialized, val syncState: SyncState = SyncState.Idle, From dfbe1188f016eea2b041e004fad24beef7c127fe Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.org> Date: Wed, 23 Sep 2020 13:43:34 +0200 Subject: [PATCH 13/17] Draft: use session realm provider when possible --- .../sdk/internal/session/room/draft/DraftRepository.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt index 7ca5e3c3ce..5cc6c990d9 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/draft/DraftRepository.kt @@ -26,6 +26,7 @@ import org.matrix.android.sdk.BuildConfig import org.matrix.android.sdk.api.session.room.send.UserDraft import org.matrix.android.sdk.api.util.Optional import org.matrix.android.sdk.api.util.toOptional +import org.matrix.android.sdk.internal.database.RealmSessionProvider import org.matrix.android.sdk.internal.database.mapper.DraftMapper import org.matrix.android.sdk.internal.database.model.RoomSummaryEntity import org.matrix.android.sdk.internal.database.model.UserDraftsEntity @@ -35,7 +36,8 @@ import org.matrix.android.sdk.internal.util.awaitTransaction import timber.log.Timber import javax.inject.Inject -internal class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy) { +internal class DraftRepository @Inject constructor(@SessionDatabase private val monarchy: Monarchy, + private val realmSessionProvider: RealmSessionProvider) { suspend fun saveDraft(roomId: String, userDraft: UserDraft) { monarchy.awaitTransaction { @@ -50,7 +52,7 @@ internal class DraftRepository @Inject constructor(@SessionDatabase private val } fun getDraft(roomId: String): UserDraft? { - return Realm.getInstance(monarchy.realmConfiguration).use { realm -> + return realmSessionProvider.withRealm { realm -> UserDraftsEntity.where(realm, roomId).findFirst() ?.userDrafts ?.firstOrNull() From 211a8dc27277878649efdee539e3faf0954126fe Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.org> Date: Wed, 23 Sep 2020 13:43:42 +0200 Subject: [PATCH 14/17] Draft: fix icon tint --- vector/src/main/res/layout/item_room.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index c844f10ed2..814ef645d7 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -105,6 +105,7 @@ android:layout_marginEnd="4dp" android:src="@drawable/ic_edit" android:visibility="gone" + android:tint="?riotx_text_primary" app:layout_constraintBottom_toBottomOf="@+id/roomNameView" app:layout_constraintEnd_toStartOf="@+id/roomUnreadCounterBadgeView" app:layout_constraintStart_toEndOf="@+id/roomNameView" From 3b8a0f8671048a20cab6d8b27195c001269ce2a7 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.org> Date: Wed, 23 Sep 2020 14:03:08 +0200 Subject: [PATCH 15/17] Tint swip reply icon --- .../room/detail/RoomMessageTouchHelperCallback.kt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt index ef13865374..41f386c606 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomMessageTouchHelperCallback.kt @@ -25,12 +25,15 @@ import android.view.MotionEvent import android.view.View import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.DrawableCompat import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_SWIPE import androidx.recyclerview.widget.RecyclerView import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyTouchHelperCallback import com.airbnb.epoxy.EpoxyViewHolder +import im.vector.app.R +import im.vector.app.features.themes.ThemeUtils import timber.log.Timber import kotlin.math.abs import kotlin.math.min @@ -52,7 +55,16 @@ class RoomMessageTouchHelperCallback(private val context: Context, private var replyButtonProgress: Float = 0F private var lastReplyButtonAnimationTime: Long = 0 - private var imageDrawable: Drawable = ContextCompat.getDrawable(context, actionIcon)!! + private val imageDrawable: Drawable = DrawableCompat.wrap( + ContextCompat.getDrawable(context, actionIcon)!! + ) + + init { + DrawableCompat.setTint( + imageDrawable, + ThemeUtils.getColor(context, R.attr.riotx_text_primary) + ) + } private val triggerDistance = convertToPx(100) private val minShowDistance = convertToPx(20) From bcc64fb276195ae03b7e452446e9211cc387baa0 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.org> Date: Wed, 23 Sep 2020 14:03:39 +0200 Subject: [PATCH 16/17] Update CHANGES --- CHANGES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES.md b/CHANGES.md index 4a158086a2..a3bf5d4f9c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,6 +15,7 @@ Improvements 🙌: Bugfix 🐛: - Improve support for image/audio/video/file selection with intent changes (#1376) - Fix Splash layout on small screens + - Simplifies draft management and should fix bunch of draft issues (#952, #683) Translations 🗣: - From 038e6613c08f73ce60c3c35df286aa565484c9d5 Mon Sep 17 00:00:00 2001 From: ganfra <francoisg@matrix.orgfrancoisg@matrix.orgfrancoisg@matrix.orgfrancoisg@matrix.org> Date: Mon, 28 Sep 2020 14:05:24 +0200 Subject: [PATCH 17/17] Draft: update state when saving draft to avoid loosing current text --- .../home/room/detail/RoomDetailViewModel.kt | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt index 985604a49c..ce48d8ff89 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt @@ -449,21 +449,23 @@ class RoomDetailViewModel @AssistedInject constructor( /** * Convert a send mode to a draft and save the draft */ - private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) { - withState { - when { - it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { - room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.REPLY -> { - room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.QUOTE -> { - room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } - it.sendMode is SendMode.EDIT -> { - room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) - } + private fun handleSaveDraft(action: RoomDetailAction.SaveDraft) = withState { + when { + it.sendMode is SendMode.REGULAR && !it.sendMode.fromSharing -> { + setState { copy(sendMode = it.sendMode.copy(action.draft)) } + room.saveDraft(UserDraft.REGULAR(action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.REPLY -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.REPLY(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.QUOTE -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.QUOTE(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) + } + it.sendMode is SendMode.EDIT -> { + setState { copy(sendMode = it.sendMode.copy(text = action.draft)) } + room.saveDraft(UserDraft.EDIT(it.sendMode.timelineEvent.root.eventId!!, action.draft), NoOpMatrixCallback()) } } }