Device list: remove the detail dialog: handle the actions directly in the list

This commit is contained in:
Benoit Marty 2020-01-02 15:42:42 +01:00
parent 6b2703f6ce
commit 8dff196716
6 changed files with 153 additions and 150 deletions

View File

@ -18,13 +18,18 @@ package im.vector.riotx.features.settings.devices
import android.graphics.Typeface import android.graphics.Typeface
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo import im.vector.matrix.android.internal.crypto.model.rest.DeviceInfo
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
/** /**
* A list item for Device. * A list item for Device.
@ -36,29 +41,69 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
lateinit var deviceInfo: DeviceInfo lateinit var deviceInfo: DeviceInfo
@EpoxyAttribute @EpoxyAttribute
var bold = false var currentDevice = false
@EpoxyAttribute
var buttonsVisible = false
@EpoxyAttribute @EpoxyAttribute
var itemClickAction: (() -> Unit)? = null var itemClickAction: (() -> Unit)? = null
@EpoxyAttribute
var renameClickAction: (() -> Unit)? = null
@EpoxyAttribute
var deleteClickAction: (() -> Unit)? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.root.setOnClickListener { itemClickAction?.invoke() } holder.root.setOnClickListener { itemClickAction?.invoke() }
holder.displayNameText.text = deviceInfo.displayName ?: "" holder.displayNameText.text = deviceInfo.displayName ?: ""
holder.deviceIdText.text = deviceInfo.deviceId ?: "" holder.deviceIdText.text = deviceInfo.deviceId ?: ""
if (bold) { val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
holder.displayNameText.setTypeface(null, Typeface.BOLD)
holder.deviceIdText.setTypeface(null, Typeface.BOLD) val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
} else { val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
holder.displayNameText.setTypeface(null, Typeface.NORMAL) val date = Date(ts)
holder.deviceIdText.setTypeface(null, Typeface.NORMAL)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
listOf(
holder.displayNameLabelText,
holder.displayNameText,
holder.deviceIdLabelText,
holder.deviceIdText,
holder.deviceLastSeenLabelText,
holder.deviceLastSeenText
).map {
it.setTypeface(null, if (currentDevice) Typeface.BOLD else Typeface.NORMAL)
} }
holder.buttonDelete.isVisible = !currentDevice
holder.buttons.isVisible = buttonsVisible
holder.buttonRename.setOnClickListener { renameClickAction?.invoke() }
holder.buttonDelete.setOnClickListener { deleteClickAction?.invoke() }
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {
val root by bind<View>(R.id.itemDeviceRoot) val root by bind<ViewGroup>(R.id.itemDeviceRoot)
val displayNameLabelText by bind<TextView>(R.id.itemDeviceDisplayNameLabel)
val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName) val displayNameText by bind<TextView>(R.id.itemDeviceDisplayName)
val deviceIdLabelText by bind<TextView>(R.id.itemDeviceIdLabel)
val deviceIdText by bind<TextView>(R.id.itemDeviceId) val deviceIdText by bind<TextView>(R.id.itemDeviceId)
val deviceLastSeenLabelText by bind<TextView>(R.id.itemDeviceLastSeenLabel)
val deviceLastSeenText by bind<TextView>(R.id.itemDeviceLastSeen)
val buttons by bind<View>(R.id.itemDeviceButtons)
val buttonDelete by bind<View>(R.id.itemDeviceDelete)
val buttonRename by bind<View>(R.id.itemDeviceRename)
} }
} }

View File

@ -72,8 +72,11 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
deviceItem { deviceItem {
id("device$idx") id("device$idx")
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
bold(isCurrentDevice) currentDevice(isCurrentDevice)
itemClickAction { callback?.onDeviceClicked(deviceInfo, isCurrentDevice) } buttonsVisible(deviceInfo.deviceId == state.currentExpandedDeviceId)
itemClickAction { callback?.onDeviceClicked(deviceInfo) }
renameClickAction { callback?.onRenameDevice(deviceInfo) }
deleteClickAction { callback?.onDeleteDevice(deviceInfo) }
} }
} }
} }
@ -81,8 +84,8 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
interface Callback { interface Callback {
fun retry() fun retry()
fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) fun onDeviceClicked(deviceInfo: DeviceInfo)
fun onRenameDevice(deviceInfo: DeviceInfo)
fun onDeleteDevice(deviceInfo: DeviceInfo)
} }
} }

View File

@ -36,6 +36,7 @@ import timber.log.Timber
data class DevicesViewState( data class DevicesViewState(
val myDeviceId: String = "", val myDeviceId: String = "",
val devices: Async<List<DeviceInfo>> = Uninitialized, val devices: Async<List<DeviceInfo>> = Uninitialized,
val currentExpandedDeviceId: String? = null,
val request: Async<Unit> = Uninitialized val request: Async<Unit> = Uninitialized
) : MvRxState ) : MvRxState
@ -44,6 +45,7 @@ sealed class DevicesAction : VectorViewModelAction {
data class Delete(val deviceInfo: DeviceInfo) : DevicesAction() data class Delete(val deviceInfo: DeviceInfo) : DevicesAction()
data class Password(val password: String) : DevicesAction() data class Password(val password: String) : DevicesAction()
data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction() data class Rename(val deviceInfo: DeviceInfo, val newName: String) : DevicesAction()
data class ToggleDevice(val deviceInfo: DeviceInfo) : DevicesAction()
} }
class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState, class DevicesViewModel @AssistedInject constructor(@Assisted initialState: DevicesViewState,
@ -114,10 +116,21 @@ class DevicesViewModel @AssistedInject constructor(@Assisted initialState: Devic
override fun handle(action: DevicesAction) { override fun handle(action: DevicesAction) {
return when (action) { return when (action) {
is DevicesAction.Retry -> refreshDevicesList() is DevicesAction.Retry -> refreshDevicesList()
is DevicesAction.Delete -> handleDelete(action) is DevicesAction.Delete -> handleDelete(action)
is DevicesAction.Password -> handlePassword(action) is DevicesAction.Password -> handlePassword(action)
is DevicesAction.Rename -> handleRename(action) is DevicesAction.Rename -> handleRename(action)
is DevicesAction.ToggleDevice -> handleToggleDevice(action)
}
}
private fun handleToggleDevice(action: DevicesAction.ToggleDevice) {
withState {
setState {
copy(
currentExpandedDeviceId = if (it.currentExpandedDeviceId == action.deviceInfo.deviceId) null else action.deviceInfo.deviceId
)
}
} }
} }

View File

@ -21,7 +21,6 @@ import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.widget.EditText import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
@ -36,12 +35,8 @@ import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.platform.VectorBaseActivity import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.settings.VectorSettingsSecurityPrivacyFragment
import kotlinx.android.synthetic.main.fragment_generic_recycler.* import kotlinx.android.synthetic.main.fragment_generic_recycler.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.* import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -97,63 +92,22 @@ class VectorSettingsDevicesFragment @Inject constructor(
} }
/** /**
* Display a dialog containing the device ID, the device name and the "last seen" information.<> * Display a dialog containing the device ID, the device name and the "last seen" information.
* This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog]) * This dialog allow to delete the corresponding device (see [.displayDeviceDeletionDialog])
* *
* @param deviceInfo the device information * @param deviceInfo the device information
* @param isCurrentDevice true if this is the current device * @param isCurrentDevice true if this is the current device
*/ */
override fun onDeviceClicked(deviceInfo: DeviceInfo, isCurrentDevice: Boolean) { override fun onDeviceClicked(deviceInfo: DeviceInfo) {
val builder = AlertDialog.Builder(requireActivity()) devicesViewModel.handle(DevicesAction.ToggleDevice(deviceInfo))
val inflater = requireActivity().layoutInflater }
val layout = inflater.inflate(R.layout.dialog_device_details, null)
var textView = layout.findViewById<TextView>(R.id.device_id)
textView.text = deviceInfo.deviceId override fun onDeleteDevice(deviceInfo: DeviceInfo) {
devicesViewModel.handle(DevicesAction.Delete(deviceInfo))
}
// device name override fun onRenameDevice(deviceInfo: DeviceInfo) {
textView = layout.findViewById(R.id.device_name) displayDeviceRenameDialog(deviceInfo)
val displayName = if (deviceInfo.displayName.isNullOrEmpty()) VectorSettingsSecurityPrivacyFragment.LABEL_UNAVAILABLE_DATA else deviceInfo.displayName
textView.text = displayName
// last seen info
textView = layout.findViewById(R.id.device_last_seen)
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = deviceInfo.lastSeenTs?.let { ts ->
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
val lastSeenInfo = getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
textView.text = lastSeenInfo
// title & icon
builder.setTitle(R.string.devices_details_dialog_title)
.setIcon(android.R.drawable.ic_dialog_info)
.setView(layout)
.setPositiveButton(R.string.rename) { _, _ -> displayDeviceRenameDialog(deviceInfo) }
// disable the deletion for our own device
if (!isCurrentDevice) {
builder.setNegativeButton(R.string.delete) { _, _ -> devicesViewModel.handle(DevicesAction.Delete(deviceInfo)) }
}
builder.setNeutralButton(R.string.cancel, null)
.setOnKeyListener(DialogInterface.OnKeyListener { dialog, keyCode, event ->
if (event.action == KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
dialog.cancel()
return@OnKeyListener true
}
false
})
.show()
} }
override fun retry() { override fun retry() {

View File

@ -1,66 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/device_container_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingLeft="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingRight="?dialogPreferredPadding">
<TextView
android:id="@+id/device_id_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_id"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device id" />
<TextView
android:id="@+id/device_name_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="a device name" />
<TextView
android:id="@+id/device_last_seen_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textSize="12sp"
android:textStyle="bold" />
<TextView
android:id="@+id/device_last_seen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="12sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
</LinearLayout>
</ScrollView>

View File

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/itemDeviceRoot" android:id="@+id/itemDeviceRoot"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -13,32 +12,87 @@
android:paddingEnd="16dp" android:paddingEnd="16dp"
android:paddingBottom="8dp"> android:paddingBottom="8dp">
<TextView
android:id="@+id/itemDeviceDisplayNameLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_name_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/itemDeviceDisplayName" android:id="@+id/itemDeviceDisplayName"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textColor="?riotx_text_primary" android:textColor="?riotx_text_primary"
android:textSize="16sp" android:textSize="16sp"
app:layout_constraintBottom_toTopOf="@+id/itemUserName"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/itemUserAvatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed"
tools:text="Android phone" /> tools:text="Android phone" />
<TextView
android:id="@+id/itemDeviceIdLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_id_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView <TextView
android:id="@+id/itemDeviceId" android:id="@+id/itemDeviceId"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"
android:textColor="?riotx_text_secondary" android:textColor="?riotx_text_primary"
android:textSize="15sp" android:textSize="15sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/itemUserAvatar"
app:layout_constraintTop_toBottomOf="@+id/itemUserId"
tools:text="XUIDERFZAA" /> tools:text="XUIDERFZAA" />
<TextView
android:id="@+id/itemDeviceLastSeenLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:text="@string/devices_details_last_seen_title"
android:textColor="?riotx_text_secondary"
android:textSize="12sp" />
<TextView
android:id="@+id/itemDeviceLastSeen"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?riotx_text_primary"
android:textSize="15sp"
tools:text="x.x.x.x @ 01/01 00:00:00" />
<LinearLayout
android:id="@+id/itemDeviceButtons"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="4dp"
android:orientation="horizontal"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceRename"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rename" />
<com.google.android.material.button.MaterialButton
android:id="@+id/itemDeviceDelete"
style="@style/VectorButtonStyleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:text="@string/delete"
android:textColor="@color/riotx_notice"
android:visibility="gone"
tools:visibility="visible" />
</LinearLayout>
</LinearLayout> </LinearLayout>