From 64717872327339e9d44752dad3319dd645e587d5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 5 Feb 2020 17:54:40 +0100 Subject: [PATCH] Share: start managing multi selection and warning --- vector/src/main/AndroidManifest.xml | 8 +++- .../home/room/detail/RoomDetailFragment.kt | 5 ++- .../features/home/room/list/RoomListAction.kt | 5 ++- .../home/room/list/RoomListFragment.kt | 38 +++++++++++++------ .../home/room/list/RoomListViewEvents.kt | 3 +- .../home/room/list/RoomListViewModel.kt | 31 ++++++++++++++- .../home/room/list/RoomListViewState.kt | 4 +- .../home/room/list/RoomSummaryController.kt | 15 +++++--- .../home/room/list/RoomSummaryItem.kt | 23 ++++++++++- .../home/room/list/RoomSummaryItemFactory.kt | 9 +++-- .../features/share/IncomingShareActivity.kt | 1 + .../res/layout/activity_incoming_share.xml | 1 - .../main/res/layout/fragment_room_list.xml | 13 +++++++ vector/src/main/res/layout/item_room.xml | 34 ++++++++++++----- vector/src/main/res/values/strings_riotX.xml | 1 + 15 files changed, 151 insertions(+), 40 deletions(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index f908880e6f..d1873b650e 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -88,7 +88,13 @@ - + + + diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt index 71ab8f2852..1caada8ce6 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/detail/RoomDetailFragment.kt @@ -154,6 +154,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.android.parcel.Parcelize import kotlinx.android.synthetic.main.fragment_room_detail.* +import kotlinx.android.synthetic.main.merge_composer_layout.* import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import org.commonmark.parser.Parser @@ -300,7 +301,9 @@ class RoomDetailFragment @Inject constructor( super.onActivityCreated(savedInstanceState) if (savedInstanceState == null) { when (val sharedData = roomDetailArgs.sharedData) { - is SharedData.Text -> roomDetailViewModel.handle(RoomDetailAction.SendMessage(sharedData.text, false)) + is SharedData.Text -> { + roomDetailViewModel.handle(RoomDetailAction.ExitSpecialMode(composerLayout.text.toString())) + } is SharedData.Attachments -> roomDetailViewModel.handle(RoomDetailAction.SendMedia(sharedData.attachmentData)) null -> Timber.v("No share data to process") } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt index 9db7374169..801bb48aed 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListAction.kt @@ -16,17 +16,20 @@ package im.vector.riotx.features.home.room.list +import im.vector.matrix.android.api.session.content.ContentAttachmentData import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.notification.RoomNotificationState import im.vector.riotx.core.platform.VectorViewModelAction +import im.vector.riotx.features.share.SharedData sealed class RoomListAction : VectorViewModelAction { - data class SelectRoom(val roomSummary: RoomSummary) : RoomListAction() + data class SelectRoom(val roomSummary: RoomSummary, val enableMultiSelect: Boolean) : RoomListAction() data class ToggleCategory(val category: RoomCategory) : RoomListAction() data class AcceptInvitation(val roomSummary: RoomSummary) : RoomListAction() data class RejectInvitation(val roomSummary: RoomSummary) : RoomListAction() data class FilterWith(val filter: String) : RoomListAction() data class ChangeRoomNotificationState(val roomId: String, val notificationState: RoomNotificationState) : RoomListAction() data class LeaveRoom(val roomId: String) : RoomListAction() + data class ShareToSelectedRooms(val sharedData: SharedData, val optionalMessage: String? = null): RoomListAction() object MarkAllRoomsRead : RoomListAction() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt index 5a32f4b8b3..8bb8eeb75c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListFragment.kt @@ -106,10 +106,15 @@ class RoomListFragment @Inject constructor( when (it) { is RoomListViewEvents.Loading -> showLoading(it.message) is RoomListViewEvents.Failure -> showFailure(it.throwable) - is RoomListViewEvents.SelectRoom -> openSelectedRoom(it) + is RoomListViewEvents.SelectRoom -> handleSelectRoom(it) }.exhaustive } + sendShareButton.setOnClickListener { _ -> + roomListViewModel.handle(RoomListAction.ShareToSelectedRooms(roomListParams.sharedData!!)) + requireActivity().finish() + } + createChatFabMenu.listener = this sharedActionViewModel @@ -131,12 +136,19 @@ class RoomListFragment @Inject constructor( super.onDestroyView() } - private fun openSelectedRoom(event: RoomListViewEvents.SelectRoom) { + private fun handleSelectRoom(event: RoomListViewEvents.SelectRoom) { if (roomListParams.displayMode == RoomListDisplayMode.SHARE) { val sharedData = roomListParams.sharedData ?: return - navigator.openRoomForSharing(requireActivity(), event.roomId, sharedData) + AlertDialog.Builder(requireActivity()) + .setTitle(R.string.send_attachment) + .setMessage(getString(R.string.share_confirm_room, event.roomSummary.displayName)) + .setPositiveButton(R.string.send) { _, _ -> + navigator.openRoomForSharing(requireActivity(), event.roomSummary.roomId, sharedData) + } + .setNegativeButton(R.string.cancel, null) + .show() } else { - navigator.openRoom(requireActivity(), event.roomId) + navigator.openRoom(requireActivity(), event.roomSummary.roomId) } } @@ -256,7 +268,7 @@ class RoomListFragment @Inject constructor( is Fail -> renderFailure(state.asyncFilteredRooms.error) } roomController.update(state) - + sendShareButton.isVisible = state.multiSelectionEnabled // Mark all as read menu when (roomListParams.displayMode) { RoomListDisplayMode.HOME, @@ -338,22 +350,24 @@ class RoomListFragment @Inject constructor( if (createChatFabMenu.onBackPressed()) { return true } - return false } // RoomSummaryController.Callback ************************************************************** override fun onRoomClicked(room: RoomSummary) { - roomListViewModel.handle(RoomListAction.SelectRoom(room)) + roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = false)) } override fun onRoomLongClicked(room: RoomSummary): Boolean { - roomController.onRoomLongClicked() - - RoomListQuickActionsBottomSheet - .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) - .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") + if (roomListParams.displayMode == RoomListDisplayMode.SHARE) { + roomListViewModel.handle(RoomListAction.SelectRoom(room, enableMultiSelect = true)) + } else { + roomController.onRoomLongClicked() + RoomListQuickActionsBottomSheet + .newInstance(room.roomId, RoomListActionsArgs.Mode.FULL) + .show(childFragmentManager, "ROOM_LIST_QUICK_ACTIONS") + } return true } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt index 2e147293ec..1798174ef0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewEvents.kt @@ -17,6 +17,7 @@ package im.vector.riotx.features.home.room.list +import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.riotx.core.platform.VectorViewEvents /** @@ -26,5 +27,5 @@ sealed class RoomListViewEvents : VectorViewEvents { data class Loading(val message: CharSequence? = null) : RoomListViewEvents() data class Failure(val throwable: Throwable) : RoomListViewEvents() - data class SelectRoom(val roomId: String) : RoomListViewEvents() + data class SelectRoom(val roomSummary: RoomSummary) : RoomListViewEvents() } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt index 22c18e9134..7ee641236c 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewModel.kt @@ -25,9 +25,11 @@ import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.tag.RoomTag +import im.vector.riotx.core.extensions.exhaustive import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.utils.DataSource import im.vector.riotx.features.home.RoomListDisplayMode +import im.vector.riotx.features.share.SharedData import io.reactivex.schedulers.Schedulers import timber.log.Timber import javax.inject.Inject @@ -67,13 +69,38 @@ class RoomListViewModel @Inject constructor(initialState: RoomListViewState, is RoomListAction.MarkAllRoomsRead -> handleMarkAllRoomsRead() is RoomListAction.LeaveRoom -> handleLeaveRoom(action) is RoomListAction.ChangeRoomNotificationState -> handleChangeNotificationMode(action) + is RoomListAction.ShareToSelectedRooms -> handleShareToSelectedRooms(action) + }.exhaustive + } + + private fun handleShareToSelectedRooms(action: RoomListAction.ShareToSelectedRooms) = withState { + val sharedData = action.sharedData + it.selectedRoomIds.forEach { roomId -> + val room = session.getRoom(roomId) + if (sharedData is SharedData.Text) { + room?.sendTextMessage(sharedData.text) + } else if (sharedData is SharedData.Attachments) { + room?.sendMedias(sharedData.attachmentData) + } } } // PRIVATE METHODS ***************************************************************************** - private fun handleSelectRoom(action: RoomListAction.SelectRoom) { - _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary.roomId)) + private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState { + if (it.multiSelectionEnabled) { + val selectedRooms = it.selectedRoomIds + val newSelectedRooms = if (selectedRooms.contains(action.roomSummary.roomId)) { + selectedRooms.minus(action.roomSummary.roomId) + } else { + selectedRooms.plus(action.roomSummary.roomId) + } + setState { copy(multiSelectionEnabled = newSelectedRooms.isNotEmpty(), selectedRoomIds = newSelectedRooms) } + } else if (action.enableMultiSelect) { + setState { copy(multiSelectionEnabled = true, selectedRoomIds = setOf(action.roomSummary.roomId)) } + } else { + _viewEvents.post(RoomListViewEvents.SelectRoom(action.roomSummary)) + } } private fun handleToggleCategory(action: RoomListAction.ToggleCategory) = setState { diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt index c127fa10e2..e660cc47c0 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomListViewState.kt @@ -46,7 +46,9 @@ data class RoomListViewState( val isServerNoticeRoomsExpanded: Boolean = true, // For sharing val isRecentExpanded: Boolean = true, - val isOtherExpanded: Boolean = true + val isOtherExpanded: Boolean = true, + val selectedRoomIds: Set = emptySet(), + val multiSelectionEnabled: Boolean = false ) : MvRxState { constructor(args: RoomListParams) : this(displayMode = args.displayMode) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt index c4afd442ab..eb05802d6e 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryController.kt @@ -77,7 +77,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri viewState.joiningRoomsIds, viewState.joiningErrorRoomsIds, viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.rejectingErrorRoomsIds, + viewState.selectedRoomIds) addFilterFooter(viewState) } @@ -85,7 +86,6 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri private fun buildShareRooms(viewState: RoomListViewState) { var hasResult = false val roomSummaries = viewState.asyncFilteredRooms() - roomListNameFilter.filter = viewState.roomFilter roomSummaries?.forEach { (category, summaries) -> @@ -105,7 +105,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri emptySet(), emptySet(), emptySet(), - emptySet() + emptySet(), + viewState.selectedRoomIds ) } } @@ -131,7 +132,8 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri viewState.joiningRoomsIds, viewState.joiningErrorRoomsIds, viewState.rejectingRoomsIds, - viewState.rejectingErrorRoomsIds) + viewState.rejectingErrorRoomsIds, + viewState.selectedRoomIds) // Never set showHelp to true for invitation if (category != RoomCategory.INVITE) { showHelp = userPreferencesProvider.shouldShowLongClickOnRoomHelp() @@ -196,10 +198,11 @@ class RoomSummaryController @Inject constructor(private val stringProvider: Stri joiningRoomsIds: Set, joiningErrorRoomsIds: Set, rejectingRoomsIds: Set, - rejectingErrorRoomsIds: Set) { + rejectingErrorRoomsIds: Set, + selectedRoomIds: Set) { summaries.forEach { roomSummary -> roomSummaryItemFactory - .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener) + .create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds,selectedRoomIds, listener) .addTo(this) } } diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt index 652740c0b7..3d4ce6b7ff 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItem.kt @@ -16,14 +16,17 @@ package im.vector.riotx.features.home.room.list +import android.view.HapticFeedbackConstants import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyModelClass +import com.amulyakhare.textdrawable.TextDrawable import im.vector.matrix.android.api.crypto.RoomEncryptionTrustLevel import im.vector.matrix.android.api.util.MatrixItem import im.vector.riotx.R @@ -48,11 +51,15 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { @EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var itemLongClickListener: View.OnLongClickListener? = null @EpoxyAttribute var itemClickListener: View.OnClickListener? = null + @EpoxyAttribute var showSelected: Boolean = false override fun bind(holder: Holder) { super.bind(holder) holder.rootView.setOnClickListener(itemClickListener) - holder.rootView.setOnLongClickListener(itemLongClickListener) + holder.rootView.setOnLongClickListener { + it.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + itemLongClickListener?.onLongClick(it) ?: false + } holder.titleView.text = matrixItem.getBestName() holder.lastEventTimeView.text = lastEventTime holder.lastEventView.text = lastFormattedEvent @@ -64,6 +71,19 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { avatarRenderer.render(matrixItem, holder.avatarImageView) holder.roomAvatarDecorationImageView.isVisible = encryptionTrustLevel != null holder.roomAvatarDecorationImageView.setImageResource(encryptionTrustLevel.toImageRes()) + renderSelection(holder, showSelected) + } + + private fun renderSelection(holder: Holder, isSelected: Boolean) { + if (isSelected) { + holder.avatarCheckedImageView.visibility = View.VISIBLE + val backgroundColor = ContextCompat.getColor(holder.view.context, R.color.riotx_accent) + val backgroundDrawable = TextDrawable.builder().buildRound("", backgroundColor) + holder.avatarImageView.setImageDrawable(backgroundDrawable) + } else { + holder.avatarCheckedImageView.visibility = View.GONE + avatarRenderer.render(matrixItem, holder.avatarImageView) + } } class Holder : VectorEpoxyHolder() { @@ -74,6 +94,7 @@ abstract class RoomSummaryItem : VectorEpoxyModel() { val typingView by bind(R.id.roomTypingView) val draftView by bind(R.id.roomDraftBadge) val lastEventTimeView by bind(R.id.roomLastEventTimeView) + val avatarCheckedImageView by bind(R.id.roomAvatarCheckedImageView) val avatarImageView by bind(R.id.roomAvatarImageView) val roomAvatarDecorationImageView by bind(R.id.roomAvatarDecorationImageView) val rootView by bind(R.id.itemRoomLayout) diff --git a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt index d224ccec47..b61f8fcbe5 100644 --- a/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt +++ b/vector/src/main/java/im/vector/riotx/features/home/room/list/RoomSummaryItemFactory.kt @@ -36,7 +36,6 @@ import javax.inject.Inject class RoomSummaryItemFactory @Inject constructor(private val displayableEventFormatter: DisplayableEventFormatter, private val dateFormatter: VectorDateFormatter, - private val colorProvider: ColorProvider, private val stringProvider: StringProvider, private val typingHelper: TypingHelper, private val session: Session, @@ -47,10 +46,11 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor joiningErrorRoomsIds: Set, rejectingRoomsIds: Set, rejectingErrorRoomsIds: Set, + selectedRoomIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { return when (roomSummary.membership) { Membership.INVITE -> createInvitationItem(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener) - else -> createRoomItem(roomSummary, listener) + else -> createRoomItem(roomSummary, selectedRoomIds, listener) } } @@ -82,10 +82,10 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .listener { listener?.onRoomClicked(roomSummary) } } - private fun createRoomItem(roomSummary: RoomSummary, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { + private fun createRoomItem(roomSummary: RoomSummary, selectedRoomIds: Set, listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { val unreadCount = roomSummary.notificationCount val showHighlighted = roomSummary.highlightCount > 0 - + val showSelected = selectedRoomIds.contains(roomSummary.roomId) var latestFormattedEvent: CharSequence = "" var latestEventTime: CharSequence = "" val latestEvent = roomSummary.latestPreviewableEvent @@ -119,6 +119,7 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor .typingString(typingString) .lastFormattedEvent(latestFormattedEvent) .showHighlighted(showHighlighted) + .showSelected(showSelected) .unreadNotificationCount(unreadCount) .hasUnreadMessage(roomSummary.hasUnreadMessages) .hasDraft(roomSummary.userDrafts.isNotEmpty()) diff --git a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt index 3669a51937..b9a9bdc103 100644 --- a/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt +++ b/vector/src/main/java/im/vector/riotx/features/share/IncomingShareActivity.kt @@ -19,6 +19,7 @@ package im.vector.riotx.features.share import android.content.ClipDescription import android.content.Intent import android.os.Bundle +import android.view.Window import android.widget.Toast import androidx.appcompat.widget.SearchView import com.airbnb.mvrx.viewModel diff --git a/vector/src/main/res/layout/activity_incoming_share.xml b/vector/src/main/res/layout/activity_incoming_share.xml index 986a852b5a..031e1b958e 100644 --- a/vector/src/main/res/layout/activity_incoming_share.xml +++ b/vector/src/main/res/layout/activity_incoming_share.xml @@ -13,7 +13,6 @@ style="@style/VectorToolbarStyle" android:layout_width="0dp" android:layout_height="?attr/actionBarSize" - android:elevation="4dp" app:contentInsetStart="0dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/vector/src/main/res/layout/fragment_room_list.xml b/vector/src/main/res/layout/fragment_room_list.xml index 2c828c8397..54e6db72e7 100644 --- a/vector/src/main/res/layout/fragment_room_list.xml +++ b/vector/src/main/res/layout/fragment_room_list.xml @@ -55,4 +55,17 @@ tools:layout_marginEnd="144dp" tools:visibility="visible" /> + + diff --git a/vector/src/main/res/layout/item_room.xml b/vector/src/main/res/layout/item_room.xml index 3ae16ddca9..05a2ff4ca9 100644 --- a/vector/src/main/res/layout/item_room.xml +++ b/vector/src/main/res/layout/item_room.xml @@ -21,21 +21,37 @@ app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> - + app:layout_constraintTop_toTopOf="parent"> + + + + + + diff --git a/vector/src/main/res/values/strings_riotX.xml b/vector/src/main/res/values/strings_riotX.xml index 75790348de..8799d56ce6 100644 --- a/vector/src/main/res/values/strings_riotX.xml +++ b/vector/src/main/res/values/strings_riotX.xml @@ -21,6 +21,7 @@ Removeā€¦ + Do you want to send this attachment to %1$s?