Adding last seen details + fix observation of wrong deviceId in ViewModel

This commit is contained in:
Maxime NATUREL 2022-09-02 10:02:54 +02:00
parent 3eaf5f7fe0
commit bbe238e9c6
12 changed files with 91 additions and 20 deletions

View file

@ -3247,5 +3247,6 @@
</plurals> </plurals>
<string name="device_manager_current_session_title">Current Session</string> <string name="device_manager_current_session_title">Current Session</string>
<string name="device_manager_session_title">Session</string> <string name="device_manager_session_title">Session</string>
<string name="device_manager_session_last_activity">Last activity %1$s</string>
</resources> </resources>

View file

@ -31,6 +31,7 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.dialogs.ManuallyVerifyDialog import im.vector.app.core.dialogs.ManuallyVerifyDialog
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSettingsDevicesBinding import im.vector.app.databinding.FragmentSettingsDevicesBinding
@ -55,6 +56,8 @@ class VectorSettingsDevicesFragment :
@Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator @Inject lateinit var viewNavigator: VectorSettingsDevicesViewNavigator
@Inject lateinit var dateFormatter: VectorDateFormatter
private val viewModel: DevicesViewModel by fragmentViewModel() private val viewModel: DevicesViewModel by fragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSettingsDevicesBinding {
@ -214,7 +217,7 @@ class VectorSettingsDevicesFragment :
isCurrentSession = true, isCurrentSession = true,
deviceFullInfo = it deviceFullInfo = it
) )
views.deviceListCurrentSession.render(viewState) views.deviceListCurrentSession.render(viewState, dateFormatter)
views.deviceListCurrentSession.debouncedClicks { views.deviceListCurrentSession.debouncedClicks {
currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) } currentDeviceInfo.deviceInfo.deviceId?.let { deviceId -> navigateToSessionOverview(deviceId) }
} }
@ -226,10 +229,10 @@ class VectorSettingsDevicesFragment :
} }
} }
private fun navigateToSessionOverview(sessionId: String) { private fun navigateToSessionOverview(deviceId: String) {
viewNavigator.navigateToSessionOverview( viewNavigator.navigateToSessionOverview(
context = requireActivity(), context = requireActivity(),
sessionId = sessionId deviceId = deviceId
) )
} }

View file

@ -22,7 +22,7 @@ import javax.inject.Inject
class VectorSettingsDevicesViewNavigator @Inject constructor() { class VectorSettingsDevicesViewNavigator @Inject constructor() {
fun navigateToSessionOverview(context: Context, sessionId: String) { fun navigateToSessionOverview(context: Context, deviceId: String) {
context.startActivity(SessionOverviewActivity.newIntent(context, sessionId)) context.startActivity(SessionOverviewActivity.newIntent(context, deviceId))
} }
} }

View file

@ -19,11 +19,15 @@ package im.vector.app.features.settings.devices.v2.list
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.databinding.ViewSessionInfoBinding import im.vector.app.databinding.ViewSessionInfoBinding
import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.themes.ThemeUtils
import org.matrix.android.sdk.api.session.crypto.model.DeviceInfo
import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel import org.matrix.android.sdk.api.session.crypto.model.RoomEncryptionTrustLevel
class SessionInfoView @JvmOverloads constructor( class SessionInfoView @JvmOverloads constructor(
@ -43,13 +47,14 @@ class SessionInfoView @JvmOverloads constructor(
val viewDetailsButton = views.sessionInfoViewDetailsButton val viewDetailsButton = views.sessionInfoViewDetailsButton
fun render(sessionInfoViewState: SessionInfoViewState) { fun render(sessionInfoViewState: SessionInfoViewState, dateFormatter: VectorDateFormatter) {
renderDeviceInfo(sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty()) renderDeviceInfo(sessionInfoViewState.deviceFullInfo.deviceInfo.displayName.orEmpty())
renderVerificationStatus( renderVerificationStatus(
sessionInfoViewState.deviceFullInfo.trustLevelForShield, sessionInfoViewState.deviceFullInfo.trustLevelForShield,
sessionInfoViewState.isCurrentSession, sessionInfoViewState.isCurrentSession,
sessionInfoViewState.hasLearnMoreLink sessionInfoViewState.isLearnMoreLinkVisible
) )
renderDeviceLastSeenDetails(sessionInfoViewState.deviceFullInfo.deviceInfo, dateFormatter, sessionInfoViewState.isLastSeenDetailsVisible)
renderDetailsButton(sessionInfoViewState.isDetailsButtonVisible) renderDetailsButton(sessionInfoViewState.isDetailsButtonVisible)
} }
@ -117,6 +122,33 @@ class SessionInfoView @JvmOverloads constructor(
views.sessionInfoNameTextView.text = sessionName views.sessionInfoNameTextView.text = sessionName
} }
private fun renderDeviceLastSeenDetails(
deviceInfo: DeviceInfo,
dateFormatter: VectorDateFormatter,
isLastSeenDetailsVisible: Boolean,
) {
deviceInfo.lastSeenTs
?.takeIf { isLastSeenDetailsVisible }
?.let { timestamp ->
views.sessionInfoLastActivityTextView.isVisible = true
val formattedTs = dateFormatter.format(timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
views.sessionInfoLastActivityTextView.text = context.getString(R.string.device_manager_session_last_activity, formattedTs)
}
?: run {
views.sessionInfoLastActivityTextView.isGone = true
}
deviceInfo.lastSeenIp
?.takeIf { isLastSeenDetailsVisible }
?.let { ipAddress ->
views.sessionInfoLastIPAddressTextView.isVisible = true
views.sessionInfoLastIPAddressTextView.text = ipAddress
}
?: run {
views.sessionInfoLastIPAddressTextView.isGone = true
}
}
private fun renderDetailsButton(isDetailsButtonVisible: Boolean) { private fun renderDetailsButton(isDetailsButtonVisible: Boolean) {
views.sessionInfoViewDetailsButton.isVisible = isDetailsButtonVisible views.sessionInfoViewDetailsButton.isVisible = isDetailsButtonVisible
} }

View file

@ -22,5 +22,6 @@ data class SessionInfoViewState(
val isCurrentSession: Boolean, val isCurrentSession: Boolean,
val deviceFullInfo: DeviceFullInfo, val deviceFullInfo: DeviceFullInfo,
val isDetailsButtonVisible: Boolean = true, val isDetailsButtonVisible: Boolean = true,
val hasLearnMoreLink: Boolean = false val isLearnMoreLinkVisible: Boolean = false,
val isLastSeenDetailsVisible: Boolean = false,
) )

View file

@ -43,9 +43,9 @@ class SessionOverviewActivity : SimpleFragmentActivity() {
} }
companion object { companion object {
fun newIntent(context: Context, sessionId: String): Intent { fun newIntent(context: Context, deviceId: String): Intent {
return Intent(context, SessionOverviewActivity::class.java).apply { return Intent(context, SessionOverviewActivity::class.java).apply {
putExtra(Mavericks.KEY_ARG, SessionOverviewArgs(sessionId)) putExtra(Mavericks.KEY_ARG, SessionOverviewArgs(deviceId))
} }
} }
} }

View file

@ -21,5 +21,5 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class SessionOverviewArgs( data class SessionOverviewArgs(
val sessionId: String val deviceId: String
) : Parcelable ) : Parcelable

View file

@ -29,10 +29,12 @@ import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState import com.airbnb.mvrx.withState
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.databinding.FragmentSessionOverviewBinding import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.DeviceFullInfo
import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
import javax.inject.Inject
/** /**
* Display the overview info about a Session. * Display the overview info about a Session.
@ -41,6 +43,8 @@ import im.vector.app.features.settings.devices.v2.list.SessionInfoViewState
class SessionOverviewFragment : class SessionOverviewFragment :
VectorBaseFragment<FragmentSessionOverviewBinding>() { VectorBaseFragment<FragmentSessionOverviewBinding>() {
@Inject lateinit var dateFormatter: VectorDateFormatter
private val viewModel: SessionOverviewViewModel by fragmentViewModel() private val viewModel: SessionOverviewViewModel by fragmentViewModel()
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSessionOverviewBinding {
@ -89,9 +93,10 @@ class SessionOverviewFragment :
isCurrentSession = isCurrentSession, isCurrentSession = isCurrentSession,
deviceFullInfo = deviceFullInfo, deviceFullInfo = deviceFullInfo,
isDetailsButtonVisible = false, isDetailsButtonVisible = false,
hasLearnMoreLink = true isLearnMoreLinkVisible = true,
isLastSeenDetailsVisible = true,
) )
views.sessionOverviewInfo.render(viewState) views.sessionOverviewInfo.render(viewState, dateFormatter)
} }
private fun hideSessionInfo() { private fun hideSessionInfo() {

View file

@ -46,10 +46,10 @@ class SessionOverviewViewModel @AssistedInject constructor(
init { init {
val currentDeviceId = session.sessionParams.deviceId.orEmpty() val currentDeviceId = session.sessionParams.deviceId.orEmpty()
setState { setState {
copy(isCurrentSession = sessionId.isNotEmpty() && sessionId == currentDeviceId) copy(isCurrentSession = deviceId.isNotEmpty() && deviceId == currentDeviceId)
} }
observeSessionInfo(currentDeviceId) observeSessionInfo(initialState.deviceId)
} }
private fun observeSessionInfo(deviceId: String) { private fun observeSessionInfo(deviceId: String) {

View file

@ -22,11 +22,11 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.settings.devices.DeviceFullInfo import im.vector.app.features.settings.devices.DeviceFullInfo
data class SessionOverviewViewState( data class SessionOverviewViewState(
val sessionId: String, val deviceId: String,
val isCurrentSession: Boolean = false, val isCurrentSession: Boolean = false,
val deviceInfo: Async<DeviceFullInfo> = Uninitialized, val deviceInfo: Async<DeviceFullInfo> = Uninitialized,
) : MavericksState { ) : MavericksState {
constructor(args: SessionOverviewArgs) : this( constructor(args: SessionOverviewArgs) : this(
sessionId = args.sessionId deviceId = args.deviceId
) )
} }

View file

@ -72,6 +72,35 @@
app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusContainer" app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusContainer"
tools:text="@string/device_manager_verification_status_detail_current_session_verified" /> tools:text="@string/device_manager_verification_status_detail_current_session_verified" />
<TextView
android:id="@+id/sessionInfoLastActivityTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:layout_marginTop="12dp"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusDetailTextView"
tools:text="Last activity Fri 14:59" />
<TextView
android:id="@+id/sessionInfoLastIPAddressTextView"
style="@style/TextAppearance.Vector.Body.DevicesManagement"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="32dp"
android:gravity="center"
android:visibility="gone"
tools:visibility="visible"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessionInfoLastActivityTextView"
tools:text="81.235.41.100 (United Kingdom)" />
<Button <Button
android:id="@+id/sessionInfoVerifySessionButton" android:id="@+id/sessionInfoVerifySessionButton"
android:layout_width="0dp" android:layout_width="0dp"
@ -81,7 +110,7 @@
android:text="@string/device_manager_verify_session" android:text="@string/device_manager_verify_session"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/sessionInfoVerificationStatusDetailTextView" /> app:layout_constraintTop_toBottomOf="@id/sessionInfoLastIPAddressTextView" />
<Button <Button
android:id="@+id/sessionInfoViewDetailsButton" android:id="@+id/sessionInfoViewDetailsButton"

View file

@ -39,7 +39,7 @@ class SessionOverviewViewModelTest {
val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher()) val mvRxTestRule = MvRxTestRule(testDispatcher = UnconfinedTestDispatcher())
private val args = SessionOverviewArgs( private val args = SessionOverviewArgs(
sessionId = A_SESSION_ID deviceId = A_SESSION_ID
) )
private val fakeSession = FakeSession() private val fakeSession = FakeSession()
private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>() private val getDeviceFullInfoUseCase = mockk<GetDeviceFullInfoUseCase>()
@ -56,7 +56,7 @@ class SessionOverviewViewModelTest {
val deviceFullInfo = mockk<DeviceFullInfo>() val deviceFullInfo = mockk<DeviceFullInfo>()
every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(Optional(deviceFullInfo)) every { getDeviceFullInfoUseCase.execute(A_SESSION_ID) } returns flowOf(Optional(deviceFullInfo))
val expectedState = SessionOverviewViewState( val expectedState = SessionOverviewViewState(
sessionId = A_SESSION_ID, deviceId = A_SESSION_ID,
isCurrentSession = true, isCurrentSession = true,
deviceInfo = Success(deviceFullInfo) deviceInfo = Success(deviceFullInfo)
) )