Skip to content

Commit e8bf799

Browse files
authored
Merge pull request #7408 from vector-im/feature/mna/session_manager_multi_selection
[Session manager] Multi selection in sessions list (PSG-852)
2 parents b7e0d93 + e765575 commit e8bf799

File tree

18 files changed

+604
-30
lines changed

18 files changed

+604
-30
lines changed

changelog.d/7396.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Multi selection in sessions list

library/ui-strings/src/main/res/values/strings.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<resources xmlns:tools="http://schemas.android.com/tools">
33

4+
<!-- Cross feature -->
5+
<plurals name="x_selected">
6+
<item quantity="one">%1$d selected</item>
7+
<item quantity="other">%1$d selected</item>
8+
</plurals>
9+
10+
<!-- Notice -->
411
<string name="notice_room_invite_no_invitee">%s\'s invitation</string>
512
<string name="notice_room_invite_no_invitee_by_you">Your invitation</string>
613
<string name="notice_room_created">%1$s created the room</string>
@@ -407,6 +414,8 @@
407414
<string name="action_learn_more">Learn more</string>
408415
<string name="action_next">Next</string>
409416
<string name="action_got_it">Got it</string>
417+
<string name="action_select_all">Select all</string>
418+
<string name="action_deselect_all">Deselect all</string>
410419

411420
<string name="copied_to_clipboard">Copied to clipboard</string>
412421

@@ -3328,6 +3337,7 @@
33283337
<string name="device_manager_other_sessions_no_unverified_sessions_found">No unverified sessions found.</string>
33293338
<string name="device_manager_other_sessions_no_inactive_sessions_found">No inactive sessions found.</string>
33303339
<string name="device_manager_other_sessions_clear_filter">Clear Filter</string>
3340+
<string name="device_manager_other_sessions_select">Select sessions</string>
33313341
<string name="device_manager_session_overview_signout">Sign out of this session</string>
33323342
<string name="device_manager_session_details_title">Session details</string>
33333343
<string name="device_manager_session_details_description">Application, device, and activity information.</string>

vector/src/main/java/im/vector/app/features/settings/devices/v2/DeviceFullInfo.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,5 @@ data class DeviceFullInfo(
3030
val isCurrentDevice: Boolean,
3131
val deviceExtendedInfo: DeviceExtendedInfo,
3232
val matrixClientInfo: MatrixClientInfoContent?,
33+
val isSelected: Boolean = false,
3334
)

vector/src/main/java/im/vector/app/features/settings/devices/v2/VectorSettingsDevicesFragment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,10 @@ class VectorSettingsDevicesFragment :
331331
views.waitingView.root.isVisible = isLoading
332332
}
333333

334+
override fun onOtherSessionLongClicked(deviceId: String) {
335+
// do nothing
336+
}
337+
334338
override fun onOtherSessionClicked(deviceId: String) {
335339
navigateToSessionOverview(deviceId)
336340
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionItem.kt

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package im.vector.app.features.settings.devices.v2.list
1818

1919
import android.graphics.drawable.Drawable
20+
import android.view.View
21+
import android.view.View.OnLongClickListener
2022
import android.widget.ImageView
2123
import android.widget.TextView
2224
import androidx.annotation.ColorInt
@@ -27,6 +29,8 @@ import im.vector.app.core.epoxy.ClickListener
2729
import im.vector.app.core.epoxy.VectorEpoxyHolder
2830
import im.vector.app.core.epoxy.VectorEpoxyModel
2931
import im.vector.app.core.epoxy.onClick
32+
import im.vector.app.core.resources.ColorProvider
33+
import im.vector.app.core.resources.DrawableProvider
3034
import im.vector.app.core.resources.StringProvider
3135
import im.vector.app.core.ui.views.ShieldImageView
3236
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
@@ -56,32 +60,54 @@ abstract class OtherSessionItem : VectorEpoxyModel<OtherSessionItem.Holder>(R.la
5660
@EpoxyAttribute
5761
lateinit var stringProvider: StringProvider
5862

63+
@EpoxyAttribute
64+
lateinit var colorProvider: ColorProvider
65+
66+
@EpoxyAttribute
67+
lateinit var drawableProvider: DrawableProvider
68+
69+
@EpoxyAttribute
70+
var selected: Boolean = false
71+
5972
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
6073
var clickListener: ClickListener? = null
6174

75+
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
76+
var onLongClickListener: OnLongClickListener? = null
77+
6278
private val setDeviceTypeIconUseCase = SetDeviceTypeIconUseCase()
6379

6480
override fun bind(holder: Holder) {
6581
super.bind(holder)
6682
holder.view.onClick(clickListener)
67-
if (clickListener == null) {
83+
holder.view.setOnLongClickListener(onLongClickListener)
84+
if (clickListener == null && onLongClickListener == null) {
6885
holder.view.isClickable = false
6986
}
7087

71-
setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider)
88+
holder.otherSessionDeviceTypeImageView.isSelected = selected
89+
if (selected) {
90+
val drawableColor = colorProvider.getColorFromAttribute(android.R.attr.colorBackground)
91+
val drawable = drawableProvider.getDrawable(R.drawable.ic_check_on, drawableColor)
92+
holder.otherSessionDeviceTypeImageView.setImageDrawable(drawable)
93+
} else {
94+
setDeviceTypeIconUseCase.execute(deviceType, holder.otherSessionDeviceTypeImageView, stringProvider)
95+
}
7296
holder.otherSessionVerificationStatusImageView.render(roomEncryptionTrustLevel)
7397
holder.otherSessionNameTextView.text = sessionName
7498
holder.otherSessionDescriptionTextView.text = sessionDescription
7599
sessionDescriptionColor?.let {
76100
holder.otherSessionDescriptionTextView.setTextColor(it)
77101
}
78102
holder.otherSessionDescriptionTextView.setCompoundDrawablesWithIntrinsicBounds(sessionDescriptionDrawable, null, null, null)
103+
holder.otherSessionItemBackgroundView.isSelected = selected
79104
}
80105

81106
class Holder : VectorEpoxyHolder() {
82107
val otherSessionDeviceTypeImageView by bind<ImageView>(R.id.otherSessionDeviceTypeImageView)
83108
val otherSessionVerificationStatusImageView by bind<ShieldImageView>(R.id.otherSessionVerificationStatusImageView)
84109
val otherSessionNameTextView by bind<TextView>(R.id.otherSessionNameTextView)
85110
val otherSessionDescriptionTextView by bind<TextView>(R.id.otherSessionDescriptionTextView)
111+
val otherSessionItemBackgroundView by bind<View>(R.id.otherSessionItemBackground)
86112
}
87113
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsController.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package im.vector.app.features.settings.devices.v2.list
1818

19+
import android.view.View
1920
import com.airbnb.epoxy.TypedEpoxyController
2021
import im.vector.app.R
2122
import im.vector.app.core.date.DateFormatKind
@@ -38,6 +39,7 @@ class OtherSessionsController @Inject constructor(
3839
var callback: Callback? = null
3940

4041
interface Callback {
42+
fun onItemLongClicked(deviceId: String)
4143
fun onItemClicked(deviceId: String)
4244
}
4345

@@ -70,8 +72,15 @@ class OtherSessionsController @Inject constructor(
7072
sessionDescription(description)
7173
sessionDescriptionDrawable(descriptionDrawable)
7274
sessionDescriptionColor(descriptionColor)
73-
stringProvider(this@OtherSessionsController.stringProvider)
75+
stringProvider(host.stringProvider)
76+
colorProvider(host.colorProvider)
77+
drawableProvider(host.drawableProvider)
78+
selected(device.isSelected)
7479
clickListener { device.deviceInfo.deviceId?.let { host.callback?.onItemClicked(it) } }
80+
onLongClickListener(View.OnLongClickListener {
81+
device.deviceInfo.deviceId?.let { host.callback?.onItemLongClicked(it) }
82+
true
83+
})
7584
}
7685
}
7786
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/list/OtherSessionsView.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class OtherSessionsView @JvmOverloads constructor(
4040
) : ConstraintLayout(context, attrs, defStyleAttr), OtherSessionsController.Callback {
4141

4242
interface Callback {
43+
fun onOtherSessionLongClicked(deviceId: String)
4344
fun onOtherSessionClicked(deviceId: String)
4445
fun onViewAllOtherSessionsClicked()
4546
}
@@ -107,4 +108,8 @@ class OtherSessionsView @JvmOverloads constructor(
107108
override fun onItemClicked(deviceId: String) {
108109
callback?.onOtherSessionClicked(deviceId)
109110
}
111+
112+
override fun onItemLongClicked(deviceId: String) {
113+
callback?.onOtherSessionLongClicked(deviceId)
114+
}
110115
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsAction.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,9 @@ import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterType
2121

2222
sealed class OtherSessionsAction : VectorViewModelAction {
2323
data class FilterDevices(val filterType: DeviceManagerFilterType) : OtherSessionsAction()
24+
data class EnableSelectMode(val deviceId: String?) : OtherSessionsAction()
25+
object DisableSelectMode : OtherSessionsAction()
26+
data class ToggleSelectionForDevice(val deviceId: String) : OtherSessionsAction()
27+
object SelectAll : OtherSessionsAction()
28+
object DeselectAll : OtherSessionsAction()
2429
}

vector/src/main/java/im/vector/app/features/settings/devices/v2/othersessions/OtherSessionsFragment.kt

Lines changed: 92 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ package im.vector.app.features.settings.devices.v2.othersessions
1818

1919
import android.os.Bundle
2020
import android.view.LayoutInflater
21+
import android.view.Menu
22+
import android.view.MenuItem
2123
import android.view.View
2224
import android.view.ViewGroup
25+
import androidx.activity.OnBackPressedCallback
26+
import androidx.activity.addCallback
2327
import androidx.annotation.StringRes
2428
import androidx.core.view.isVisible
2529
import com.airbnb.mvrx.Success
@@ -31,7 +35,9 @@ import im.vector.app.R
3135
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
3236
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment.ResultListener.Companion.RESULT_OK
3337
import im.vector.app.core.platform.VectorBaseFragment
38+
import im.vector.app.core.platform.VectorMenuProvider
3439
import im.vector.app.core.resources.ColorProvider
40+
import im.vector.app.core.resources.StringProvider
3541
import im.vector.app.databinding.FragmentOtherSessionsBinding
3642
import im.vector.app.features.settings.devices.v2.DeviceFullInfo
3743
import im.vector.app.features.settings.devices.v2.filter.DeviceManagerFilterBottomSheet
@@ -40,25 +46,79 @@ import im.vector.app.features.settings.devices.v2.list.OtherSessionsView
4046
import im.vector.app.features.settings.devices.v2.list.SESSION_IS_MARKED_AS_INACTIVE_AFTER_DAYS
4147
import im.vector.app.features.settings.devices.v2.more.SessionLearnMoreBottomSheet
4248
import im.vector.app.features.themes.ThemeUtils
49+
import org.matrix.android.sdk.api.extensions.orFalse
4350
import javax.inject.Inject
4451

4552
@AndroidEntryPoint
4653
class OtherSessionsFragment :
4754
VectorBaseFragment<FragmentOtherSessionsBinding>(),
4855
VectorBaseBottomSheetDialogFragment.ResultListener,
49-
OtherSessionsView.Callback {
56+
OtherSessionsView.Callback,
57+
VectorMenuProvider {
5058

5159
private val viewModel: OtherSessionsViewModel by fragmentViewModel()
5260
private val args: OtherSessionsArgs by args()
5361

5462
@Inject lateinit var colorProvider: ColorProvider
5563

64+
@Inject lateinit var stringProvider: StringProvider
65+
5666
@Inject lateinit var viewNavigator: OtherSessionsViewNavigator
5767

5868
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentOtherSessionsBinding {
5969
return FragmentOtherSessionsBinding.inflate(layoutInflater, container, false)
6070
}
6171

72+
override fun getMenuRes() = R.menu.menu_other_sessions
73+
74+
override fun handlePrepareMenu(menu: Menu) {
75+
withState(viewModel) { state ->
76+
val isSelectModeEnabled = state.isSelectModeEnabled
77+
menu.findItem(R.id.otherSessionsSelectAll).isVisible = isSelectModeEnabled
78+
menu.findItem(R.id.otherSessionsDeselectAll).isVisible = isSelectModeEnabled
79+
menu.findItem(R.id.otherSessionsSelect).isVisible = !isSelectModeEnabled && state.devices()?.isNotEmpty().orFalse()
80+
}
81+
}
82+
83+
override fun handleMenuItemSelected(item: MenuItem): Boolean {
84+
return when (item.itemId) {
85+
R.id.otherSessionsSelect -> {
86+
enableSelectMode(true)
87+
true
88+
}
89+
R.id.otherSessionsSelectAll -> {
90+
viewModel.handle(OtherSessionsAction.SelectAll)
91+
true
92+
}
93+
R.id.otherSessionsDeselectAll -> {
94+
viewModel.handle(OtherSessionsAction.DeselectAll)
95+
true
96+
}
97+
else -> false
98+
}
99+
}
100+
101+
private fun enableSelectMode(isEnabled: Boolean, deviceId: String? = null) {
102+
val action = if (isEnabled) OtherSessionsAction.EnableSelectMode(deviceId) else OtherSessionsAction.DisableSelectMode
103+
viewModel.handle(action)
104+
}
105+
106+
override fun onCreate(savedInstanceState: Bundle?) {
107+
super.onCreate(savedInstanceState)
108+
activity?.onBackPressedDispatcher?.addCallback(owner = this) {
109+
handleBackPress(this)
110+
}
111+
}
112+
113+
private fun handleBackPress(onBackPressedCallback: OnBackPressedCallback) = withState(viewModel) { state ->
114+
if (state.isSelectModeEnabled) {
115+
enableSelectMode(false)
116+
} else {
117+
onBackPressedCallback.isEnabled = false
118+
activity?.onBackPressedDispatcher?.onBackPressed()
119+
}
120+
}
121+
62122
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
63123
super.onViewCreated(view, savedInstanceState)
64124
setupToolbar(views.otherSessionsToolbar).setTitle(args.titleResourceId).allowBack()
@@ -103,11 +163,24 @@ class OtherSessionsFragment :
103163

104164
override fun invalidate() = withState(viewModel) { state ->
105165
if (state.devices is Success) {
106-
renderDevices(state.devices(), state.currentFilter)
166+
val devices = state.devices.invoke()
167+
renderDevices(devices, state.currentFilter)
168+
updateToolbar(devices, state.isSelectModeEnabled)
107169
}
108170
}
109171

110-
private fun renderDevices(devices: List<DeviceFullInfo>?, currentFilter: DeviceManagerFilterType) {
172+
private fun updateToolbar(devices: List<DeviceFullInfo>, isSelectModeEnabled: Boolean) {
173+
invalidateOptionsMenu()
174+
val title = if (isSelectModeEnabled) {
175+
val selection = devices.count { it.isSelected }
176+
stringProvider.getQuantityString(R.plurals.x_selected, selection, selection)
177+
} else {
178+
getString(args.titleResourceId)
179+
}
180+
toolbar?.title = title
181+
}
182+
183+
private fun renderDevices(devices: List<DeviceFullInfo>, currentFilter: DeviceManagerFilterType) {
111184
views.otherSessionsFilterBadgeImageView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS
112185
views.otherSessionsSecurityRecommendationView.isVisible = currentFilter != DeviceManagerFilterType.ALL_SESSIONS
113186
views.deviceListHeaderOtherSessions.isVisible = currentFilter == DeviceManagerFilterType.ALL_SESSIONS
@@ -160,7 +233,7 @@ class OtherSessionsFragment :
160233
}
161234
}
162235

163-
if (devices.isNullOrEmpty()) {
236+
if (devices.isEmpty()) {
164237
views.deviceListOtherSessions.isVisible = false
165238
views.otherSessionsNotFoundLayout.isVisible = true
166239
} else {
@@ -190,11 +263,21 @@ class OtherSessionsFragment :
190263
SessionLearnMoreBottomSheet.show(childFragmentManager, args)
191264
}
192265

193-
override fun onOtherSessionClicked(deviceId: String) {
194-
viewNavigator.navigateToSessionOverview(
195-
context = requireActivity(),
196-
deviceId = deviceId
197-
)
266+
override fun onOtherSessionLongClicked(deviceId: String) = withState(viewModel) { state ->
267+
if (!state.isSelectModeEnabled) {
268+
enableSelectMode(true, deviceId)
269+
}
270+
}
271+
272+
override fun onOtherSessionClicked(deviceId: String) = withState(viewModel) { state ->
273+
if (state.isSelectModeEnabled) {
274+
viewModel.handle(OtherSessionsAction.ToggleSelectionForDevice(deviceId))
275+
} else {
276+
viewNavigator.navigateToSessionOverview(
277+
context = requireActivity(),
278+
deviceId = deviceId
279+
)
280+
}
198281
}
199282

200283
override fun onViewAllOtherSessionsClicked() {

0 commit comments

Comments
 (0)