Skip to content

Commit cec945c

Browse files
committed
Merge branch 'release/0.6.3' into main
2 parents cfc0fc9 + 7eac45e commit cec945c

File tree

96 files changed

+2597
-369
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

96 files changed

+2597
-369
lines changed

CHANGES.md

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
Changes in Element X v0.6.2 (2024-09-17)
2+
========================================
3+
4+
### ✨ Features
5+
* Account deactivation. by @bmarty in https://github.com/element-hq/element-x-android/pull/3479
6+
17
Changes in Element X v0.6.1 (2024-09-17)
28
========================================
39

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Element X is the new generation of Element for professional and personal use on mobile. It’s the fastest Matrix client with a seamless & intuitive user interface.
2+
Full changelog: https://github.com/element-hq/element-x-android/releases

features/deactivation/impl/build.gradle.kts

+1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ dependencies {
3333
implementation(projects.libraries.architecture)
3434
implementation(projects.libraries.matrix.api)
3535
implementation(projects.libraries.designsystem)
36+
implementation(projects.libraries.testtags)
3637
implementation(projects.libraries.uiStrings)
3738
api(projects.features.deactivation.api)
3839

features/deactivation/impl/src/main/kotlin/io/element/android/features/logout/impl/AccountDeactivationView.kt

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import io.element.android.libraries.designsystem.theme.components.Text
6565
import io.element.android.libraries.designsystem.theme.components.TopAppBar
6666
import io.element.android.libraries.designsystem.theme.components.autofill
6767
import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext
68+
import io.element.android.libraries.testtags.TestTags
69+
import io.element.android.libraries.testtags.testTag
6870
import io.element.android.libraries.ui.strings.CommonStrings
6971
import kotlinx.collections.immutable.persistentListOf
7072

@@ -277,6 +279,7 @@ private fun Content(
277279
.padding(top = 8.dp)
278280
.fillMaxWidth()
279281
.onTabOrEnterKeyFocusNext(focusManager)
282+
.testTag(TestTags.loginPassword)
280283
.autofill(
281284
autofillTypes = listOf(AutofillType.Password),
282285
onFill = {

features/deactivation/impl/src/test/kotlin/io/element/android/features/logout/impl/AccountDeactivationViewTest.kt

+101-1
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,26 @@ package io.element.android.features.logout.impl
1010
import androidx.activity.ComponentActivity
1111
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
1212
import androidx.compose.ui.test.junit4.createAndroidComposeRule
13+
import androidx.compose.ui.test.onNodeWithTag
14+
import androidx.compose.ui.test.performTextInput
1315
import androidx.test.ext.junit.runners.AndroidJUnit4
16+
import io.element.android.features.deactivation.impl.R
17+
import io.element.android.libraries.architecture.AsyncAction
18+
import io.element.android.libraries.matrix.test.AN_EXCEPTION
19+
import io.element.android.libraries.matrix.test.A_PASSWORD
20+
import io.element.android.libraries.testtags.TestTags
21+
import io.element.android.libraries.ui.strings.CommonStrings
1422
import io.element.android.tests.testutils.EnsureNeverCalled
1523
import io.element.android.tests.testutils.EventsRecorder
24+
import io.element.android.tests.testutils.clickOn
1625
import io.element.android.tests.testutils.ensureCalledOnce
1726
import io.element.android.tests.testutils.pressBack
27+
import io.element.android.tests.testutils.pressTag
1828
import org.junit.Rule
1929
import org.junit.Test
2030
import org.junit.rules.TestRule
2131
import org.junit.runner.RunWith
32+
import org.robolectric.annotation.Config
2233

2334
@RunWith(AndroidJUnit4::class)
2435
class AccountDeactivationViewTest {
@@ -36,7 +47,96 @@ class AccountDeactivationViewTest {
3647
}
3748
}
3849

39-
// TODO Add more tests
50+
@Config(qualifiers = "h1024dp")
51+
@Test
52+
fun `clicking on Deactivate emits the expected Event`() {
53+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
54+
rule.setAccountDeactivationView(
55+
state = anAccountDeactivationState(
56+
deactivateFormState = aDeactivateFormState(
57+
password = A_PASSWORD,
58+
),
59+
eventSink = eventsRecorder,
60+
),
61+
)
62+
rule.clickOn(CommonStrings.action_deactivate)
63+
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
64+
}
65+
66+
@Test
67+
fun `clicking on Deactivate on the confirmation dialog emits the expected Event`() {
68+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
69+
rule.setAccountDeactivationView(
70+
state = anAccountDeactivationState(
71+
deactivateFormState = aDeactivateFormState(
72+
password = A_PASSWORD,
73+
),
74+
accountDeactivationAction = AsyncAction.Confirming,
75+
eventSink = eventsRecorder,
76+
),
77+
)
78+
rule.pressTag(TestTags.dialogPositive.value)
79+
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(false))
80+
}
81+
82+
@Test
83+
fun `clicking on retry on the confirmation dialog emits the expected Event`() {
84+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
85+
rule.setAccountDeactivationView(
86+
state = anAccountDeactivationState(
87+
deactivateFormState = aDeactivateFormState(
88+
password = A_PASSWORD,
89+
),
90+
accountDeactivationAction = AsyncAction.Failure(AN_EXCEPTION),
91+
eventSink = eventsRecorder,
92+
),
93+
)
94+
rule.clickOn(CommonStrings.action_retry)
95+
eventsRecorder.assertSingle(AccountDeactivationEvents.DeactivateAccount(true))
96+
}
97+
98+
@Test
99+
fun `switching on the erase all switch emits the expected Event`() {
100+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
101+
rule.setAccountDeactivationView(
102+
state = anAccountDeactivationState(
103+
eventSink = eventsRecorder,
104+
),
105+
)
106+
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages)
107+
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(true))
108+
}
109+
110+
@Test
111+
fun `switching off the erase all switch emits the expected Event`() {
112+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
113+
rule.setAccountDeactivationView(
114+
state = anAccountDeactivationState(
115+
deactivateFormState = aDeactivateFormState(
116+
eraseData = true,
117+
),
118+
eventSink = eventsRecorder,
119+
),
120+
)
121+
rule.clickOn(R.string.screen_deactivate_account_delete_all_messages)
122+
eventsRecorder.assertSingle(AccountDeactivationEvents.SetEraseData(false))
123+
}
124+
125+
@Config(qualifiers = "h1024dp")
126+
@Test
127+
fun `typing text in the password field emits the expected Event`() {
128+
val eventsRecorder = EventsRecorder<AccountDeactivationEvents>()
129+
rule.setAccountDeactivationView(
130+
state = anAccountDeactivationState(
131+
deactivateFormState = aDeactivateFormState(
132+
password = A_PASSWORD,
133+
),
134+
eventSink = eventsRecorder,
135+
),
136+
)
137+
rule.onNodeWithTag(TestTags.loginPassword.value).performTextInput("A")
138+
eventsRecorder.assertSingle(AccountDeactivationEvents.SetPassword("A$A_PASSWORD"))
139+
}
40140
}
41141

42142
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setAccountDeactivationView(

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,8 @@ private fun VerifiedUserSendFailureView(
374374
fun VerifiedUserSendFailure.headline(): String {
375375
return when (this) {
376376
is None -> ""
377-
is UnsignedDevice -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
377+
is UnsignedDevice.FromOther -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_unsigned_device, userDisplayName)
378+
is UnsignedDevice.FromYou -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_you_unsigned_device)
378379
is ChangedIdentity -> stringResource(CommonStrings.screen_timeline_item_menu_send_failure_changed_identity, userDisplayName)
379380
}
380381
}

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailure.kt

+4-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@ import androidx.compose.runtime.Immutable
1313
sealed interface VerifiedUserSendFailure {
1414
data object None : VerifiedUserSendFailure
1515

16-
data class UnsignedDevice(
17-
val userDisplayName: String,
18-
) : VerifiedUserSendFailure
16+
sealed interface UnsignedDevice : VerifiedUserSendFailure {
17+
data object FromYou : UnsignedDevice
18+
data class FromOther(val userDisplayName: String) : UnsignedDevice
19+
}
1920

2021
data class ChangedIdentity(
2122
val userDisplayName: String,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/VerifiedUserSendFailureFactory.kt

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,12 @@ class VerifiedUserSendFailureFactory @Inject constructor(
2323
if (userId == null) {
2424
VerifiedUserSendFailure.None
2525
} else {
26-
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
27-
VerifiedUserSendFailure.UnsignedDevice(displayName)
26+
if (userId == room.sessionId) {
27+
VerifiedUserSendFailure.UnsignedDevice.FromYou
28+
} else {
29+
val displayName = room.userDisplayName(userId).getOrNull() ?: userId.value
30+
VerifiedUserSendFailure.UnsignedDevice.FromOther(displayName)
31+
}
2832
}
2933
}
3034
is LocalEventSendState.Failed.VerifiedUserChangedIdentity -> {

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureStateProvider.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ fun aResolveVerifiedUserSendFailureState(
3636
eventSink = eventSink
3737
)
3838

39-
fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice(
39+
fun anUnsignedDeviceSendFailure(userDisplayName: String = "Alice") = VerifiedUserSendFailure.UnsignedDevice.FromOther(
4040
userDisplayName = userDisplayName,
4141
)
4242

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailureView.kt

+10-5
Original file line numberDiff line numberDiff line change
@@ -113,28 +113,33 @@ fun ResolveVerifiedUserSendFailureView(
113113
@Composable
114114
private fun VerifiedUserSendFailure.title(): String {
115115
return when (this) {
116-
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_title, userDisplayName)
116+
is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource(
117+
id = CommonStrings.screen_resolve_send_failure_unsigned_device_title,
118+
userDisplayName
119+
)
120+
VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_title)
117121
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
118122
id = CommonStrings.screen_resolve_send_failure_changed_identity_title,
119123
userDisplayName
120124
)
121-
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
125+
VerifiedUserSendFailure.None -> ""
122126
}
123127
}
124128

125129
@Composable
126130
private fun VerifiedUserSendFailure.subtitle(): String {
127131
return when (this) {
128-
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(
132+
is VerifiedUserSendFailure.UnsignedDevice.FromOther -> stringResource(
129133
id = CommonStrings.screen_resolve_send_failure_unsigned_device_subtitle,
130134
userDisplayName,
131135
userDisplayName,
132136
)
137+
VerifiedUserSendFailure.UnsignedDevice.FromYou -> stringResource(id = CommonStrings.screen_resolve_send_failure_you_unsigned_device_subtitle)
133138
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(
134139
id = CommonStrings.screen_resolve_send_failure_changed_identity_subtitle,
135140
userDisplayName
136141
)
137-
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
142+
VerifiedUserSendFailure.None -> ""
138143
}
139144
}
140145

@@ -143,7 +148,7 @@ private fun VerifiedUserSendFailure.resolveAction(): String {
143148
return when (this) {
144149
is VerifiedUserSendFailure.UnsignedDevice -> stringResource(id = CommonStrings.screen_resolve_send_failure_unsigned_device_primary_button_title)
145150
is VerifiedUserSendFailure.ChangedIdentity -> stringResource(id = CommonStrings.screen_resolve_send_failure_changed_identity_primary_button_title)
146-
VerifiedUserSendFailure.None -> error("This method should never be called for this state")
151+
VerifiedUserSendFailure.None -> ""
147152
}
148153
}
149154

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt

-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,12 @@ import kotlinx.collections.immutable.ImmutableList
2525
import kotlinx.collections.immutable.toImmutableList
2626
import kotlinx.coroutines.ExperimentalCoroutinesApi
2727
import kotlinx.coroutines.FlowPreview
28-
import kotlinx.coroutines.flow.debounce
2928
import kotlinx.coroutines.flow.flatMapLatest
3029
import kotlinx.coroutines.flow.flowOf
3130
import kotlinx.coroutines.flow.launchIn
3231
import kotlinx.coroutines.flow.map
3332
import kotlinx.coroutines.flow.onEach
3433
import javax.inject.Inject
35-
import kotlin.time.Duration.Companion.milliseconds
3634

3735
class PinnedMessagesBannerPresenter @Inject constructor(
3836
private val room: MatrixRoom,
@@ -123,7 +121,6 @@ class PinnedMessagesBannerPresenter @Inject constructor(
123121
is AsyncData.Loading -> flowOf(AsyncData.Loading())
124122
is AsyncData.Success -> {
125123
asyncTimeline.data.timelineItems
126-
.debounce(300.milliseconds)
127124
.map { timelineItems ->
128125
val pinnedItems = timelineItems.mapNotNull { timelineItem ->
129126
itemFactory.create(timelineItem)

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt

+1-3
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,12 @@ import kotlinx.collections.immutable.ImmutableList
4343
import kotlinx.coroutines.CoroutineScope
4444
import kotlinx.coroutines.FlowPreview
4545
import kotlinx.coroutines.flow.combine
46-
import kotlinx.coroutines.flow.debounce
4746
import kotlinx.coroutines.flow.flowOf
4847
import kotlinx.coroutines.flow.launchIn
4948
import kotlinx.coroutines.flow.map
5049
import kotlinx.coroutines.flow.onEach
5150
import kotlinx.coroutines.launch
5251
import timber.log.Timber
53-
import kotlin.time.Duration.Companion.milliseconds
5452

5553
class PinnedMessagesListPresenter @AssistedInject constructor(
5654
@Assisted private val navigator: PinnedMessagesListNavigator,
@@ -174,7 +172,7 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
174172
is AsyncData.Failure -> flowOf(AsyncData.Failure(asyncTimeline.error))
175173
is AsyncData.Loading -> flowOf(AsyncData.Loading())
176174
is AsyncData.Success -> {
177-
val timelineItemsFlow = asyncTimeline.data.timelineItems.debounce(300.milliseconds)
175+
val timelineItemsFlow = asyncTimeline.data.timelineItems
178176
combine(timelineItemsFlow, room.membersStateFlow) { items, membersState ->
179177
timelineItemsFactory.replaceWith(
180178
timelineItems = items,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import androidx.compose.runtime.mutableStateOf
1717
import androidx.compose.runtime.remember
1818
import androidx.compose.runtime.rememberCoroutineScope
1919
import androidx.compose.runtime.saveable.rememberSaveable
20+
import androidx.compose.runtime.setValue
2021
import dagger.assisted.Assisted
2122
import dagger.assisted.AssistedFactory
2223
import dagger.assisted.AssistedInject
@@ -81,6 +82,7 @@ class TimelinePresenter @AssistedInject constructor(
8182
computeReactions = true,
8283
)
8384
)
85+
private var timelineItems by mutableStateOf<ImmutableList<TimelineItem>>(persistentListOf())
8486

8587
@Composable
8688
override fun present(): TimelineState {
@@ -89,9 +91,12 @@ class TimelinePresenter @AssistedInject constructor(
8991
mutableStateOf(FocusRequestState.None)
9092
}
9193

94+
LaunchedEffect(Unit) {
95+
timelineItemsFactory.timelineItems.collect { timelineItems = it }
96+
}
97+
9298
val lastReadReceiptId = rememberSaveable { mutableStateOf<EventId?>(null) }
9399

94-
val timelineItems by timelineItemsFactory.timelineItems.collectAsState(initial = persistentListOf())
95100
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
96101

97102
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/crypto/sendfailure/resolve/ResolveVerifiedUserSendFailurePresenterTest.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
9494
initialState.eventSink(ResolveVerifiedUserSendFailureEvents.ComputeForMessage(failedMessage))
9595
skipItems(1)
9696
awaitItem().also { state ->
97-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
97+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
9898
state.eventSink(ResolveVerifiedUserSendFailureEvents.Dismiss)
9999
}
100100
skipItems(1)
@@ -124,7 +124,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
124124

125125
skipItems(1)
126126
awaitItem().also { state ->
127-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
127+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
128128
state.eventSink(ResolveVerifiedUserSendFailureEvents.Retry)
129129
}
130130
awaitItem().also { state ->
@@ -158,7 +158,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
158158

159159
skipItems(1)
160160
awaitItem().also { state ->
161-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
161+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
162162
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
163163
}
164164
awaitItem().also { state ->
@@ -167,7 +167,7 @@ class ResolveVerifiedUserSendFailurePresenterTest {
167167
// This should move to the next user
168168
skipItems(2)
169169
awaitItem().also { state ->
170-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID_2.value))
170+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromOther(A_USER_ID_2.value))
171171
assertThat(state.resolveAction).isEqualTo(AsyncAction.Success(Unit))
172172
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
173173
}
@@ -199,14 +199,14 @@ class ResolveVerifiedUserSendFailurePresenterTest {
199199

200200
skipItems(1)
201201
awaitItem().also { state ->
202-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
202+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
203203
state.eventSink(ResolveVerifiedUserSendFailureEvents.ResolveAndResend)
204204
}
205205
awaitItem().also { state ->
206206
assertThat(state.resolveAction).isEqualTo(AsyncAction.Loading)
207207
}
208208
awaitItem().also { state ->
209-
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice(A_USER_ID.value))
209+
assertThat(state.verifiedUserSendFailure).isEqualTo(VerifiedUserSendFailure.UnsignedDevice.FromYou)
210210
assertThat(state.resolveAction).isInstanceOf(AsyncAction.Failure::class.java)
211211
}
212212
ensureAllEventsConsumed()

0 commit comments

Comments
 (0)