Share: start managing multi selection and warning

This commit is contained in:
ganfra 2020-02-05 17:54:40 +01:00 committed by Benoit Marty
parent 41f1ec5d88
commit 6471787232
15 changed files with 151 additions and 40 deletions

View file

@ -88,7 +88,13 @@
</intent-filter>
</activity>
<activity android:name=".features.share.IncomingShareActivity">
<activity
android:name=".features.share.IncomingShareActivity"
android:parentActivityName=".features.home.HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".features.home.HomeActivity" />
<intent-filter>
<action android:name="android.intent.action.SEND" />
<data android:mimeType="*/*" />

View file

@ -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")
}

View file

@ -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()
}

View file

@ -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
}

View file

@ -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()
}

View file

@ -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 {

View file

@ -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<String> = emptySet(),
val multiSelectionEnabled: Boolean = false
) : MvRxState {
constructor(args: RoomListParams) : this(displayMode = args.displayMode)

View file

@ -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<String>,
joiningErrorRoomsIds: Set<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>) {
rejectingErrorRoomsIds: Set<String>,
selectedRoomIds: Set<String>) {
summaries.forEach { roomSummary ->
roomSummaryItemFactory
.create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds, listener)
.create(roomSummary, joiningRoomsIds, joiningErrorRoomsIds, rejectingRoomsIds, rejectingErrorRoomsIds,selectedRoomIds, listener)
.addTo(this)
}
}

View file

@ -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<RoomSummaryItem.Holder>() {
@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<RoomSummaryItem.Holder>() {
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<RoomSummaryItem.Holder>() {
val typingView by bind<TextView>(R.id.roomTypingView)
val draftView by bind<ImageView>(R.id.roomDraftBadge)
val lastEventTimeView by bind<TextView>(R.id.roomLastEventTimeView)
val avatarCheckedImageView by bind<ImageView>(R.id.roomAvatarCheckedImageView)
val avatarImageView by bind<ImageView>(R.id.roomAvatarImageView)
val roomAvatarDecorationImageView by bind<ImageView>(R.id.roomAvatarDecorationImageView)
val rootView by bind<ViewGroup>(R.id.itemRoomLayout)

View file

@ -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<String>,
rejectingRoomsIds: Set<String>,
rejectingErrorRoomsIds: Set<String>,
selectedRoomIds: Set<String>,
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<String>, 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())

View file

@ -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

View file

@ -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"

View file

@ -55,4 +55,17 @@
tools:layout_marginEnd="144dp"
tools:visibility="visible" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/sendShareButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:accessibilityTraversalBefore="@+id/roomListView"
android:contentDescription="@string/a11y_create_room"
android:src="@drawable/ic_send"
android:visibility="gone"
tools:visibility="visible" />
</im.vector.riotx.core.platform.StateView>

View file

@ -21,21 +21,37 @@
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<ImageView
android:id="@+id/roomAvatarImageView"
android:layout_width="56dp"
android:layout_height="56dp"
<FrameLayout
android:id="@+id/roomAvatarContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@tools:sample/avatars" />
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/roomAvatarImageView"
android:layout_width="56dp"
android:layout_height="56dp"
tools:src="@tools:sample/avatars" />
<ImageView
android:id="@+id/roomAvatarCheckedImageView"
android:layout_width="56dp"
android:layout_height="56dp"
android:scaleType="centerInside"
android:src="@drawable/ic_material_done"
android:tint="@android:color/white"
android:visibility="visible" />
</FrameLayout>
<ImageView
android:id="@+id/roomAvatarDecorationImageView"
android:layout_width="24dp"
android:layout_height="24dp"
app:layout_constraintCircle="@+id/roomAvatarImageView"
app:layout_constraintCircle="@id/roomAvatarContainer"
app:layout_constraintCircleAngle="135"
app:layout_constraintCircleRadius="28dp"
tools:ignore="MissingConstraints"
@ -47,7 +63,7 @@
android:layout_width="0dp"
android:layout_height="12dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/roomAvatarImageView"
app:layout_constraintTop_toBottomOf="@id/roomAvatarContainer"
tools:layout_marginStart="20dp" />
<im.vector.riotx.core.platform.EllipsizingTextView
@ -69,7 +85,7 @@
app:layout_constraintEnd_toStartOf="@+id/roomDraftBadge"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/roomAvatarImageView"
app:layout_constraintStart_toEndOf="@id/roomAvatarContainer"
app:layout_constraintTop_toTopOf="parent"
tools:text="@sample/matrix.json/data/displayName" />

View file

@ -21,6 +21,7 @@
<!-- BEGIN Strings added by Benoit -->
<string name="message_action_item_redact">Remove…</string>
<string name="share_confirm_room">Do you want to send this attachment to %1$s?</string>
<!-- END Strings added by Benoit -->