ganfra
7 months ago
94 changed files with 1436 additions and 450 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
Improve how Talkback works with the timeline. Sadly, it's still not 100% working, but there is some issue with the `LazyColumn` using `reverseLayout` that only Google can fix. |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
Add moderation to rooms: |
||||
|
||||
- Sort member in room member list by powerlevel, display their roles. |
@ -0,0 +1,156 @@
@@ -0,0 +1,156 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.location.impl.show |
||||
|
||||
import androidx.activity.ComponentActivity |
||||
import androidx.compose.runtime.CompositionLocalProvider |
||||
import androidx.compose.ui.platform.LocalInspectionMode |
||||
import androidx.compose.ui.test.junit4.AndroidComposeTestRule |
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule |
||||
import androidx.compose.ui.test.onNodeWithContentDescription |
||||
import androidx.compose.ui.test.onNodeWithTag |
||||
import androidx.compose.ui.test.performClick |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import io.element.android.libraries.testtags.TestTags |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
import io.element.android.tests.testutils.EnsureNeverCalled |
||||
import io.element.android.tests.testutils.EventsRecorder |
||||
import io.element.android.tests.testutils.clickOn |
||||
import io.element.android.tests.testutils.ensureCalledOnce |
||||
import io.element.android.tests.testutils.pressBack |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import org.junit.rules.TestRule |
||||
import org.junit.runner.RunWith |
||||
|
||||
@RunWith(AndroidJUnit4::class) |
||||
class ShowLocationViewTest { |
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() |
||||
|
||||
@Test |
||||
fun `test back action`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>(expectEvents = false) |
||||
ensureCalledOnce { callback -> |
||||
rule.setShowLocationView( |
||||
state = aShowLocationState( |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = callback, |
||||
) |
||||
rule.pressBack() |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `test share action`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
val shareContentDescription = rule.activity.getString(CommonStrings.action_share) |
||||
rule.onNodeWithContentDescription(shareContentDescription).performClick() |
||||
eventsRecorder.assertSingle(ShowLocationEvents.Share) |
||||
} |
||||
|
||||
@Test |
||||
fun `test fab click`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
val shareContentDescription = rule.activity.getString(CommonStrings.action_share) |
||||
rule.onNodeWithTag(TestTags.floatingActionButton.value).performClick() |
||||
eventsRecorder.assertSingle(ShowLocationEvents.TrackMyLocation(true)) |
||||
} |
||||
|
||||
@Test |
||||
fun `when permission denied is displayed user can open the settings`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied, |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
rule.clickOn(CommonStrings.action_continue) |
||||
eventsRecorder.assertSingle(ShowLocationEvents.OpenAppSettings) |
||||
} |
||||
|
||||
@Test |
||||
fun `when permission denied is displayed user can close the dialog`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
permissionDialog = ShowLocationState.Dialog.PermissionDenied, |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
rule.clickOn(CommonStrings.action_cancel) |
||||
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog) |
||||
} |
||||
|
||||
@Test |
||||
fun `when permission rationale is displayed user can request permissions`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale, |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
rule.clickOn(CommonStrings.action_continue) |
||||
eventsRecorder.assertSingle(ShowLocationEvents.RequestPermissions) |
||||
} |
||||
|
||||
@Test |
||||
fun `when permission rationale is displayed user can close the dialog`() { |
||||
val eventsRecorder = EventsRecorder<ShowLocationEvents>() |
||||
rule.setShowLocationView( |
||||
aShowLocationState( |
||||
permissionDialog = ShowLocationState.Dialog.PermissionRationale, |
||||
eventSink = eventsRecorder |
||||
), |
||||
onBackPressed = EnsureNeverCalled(), |
||||
) |
||||
rule.clickOn(CommonStrings.action_cancel) |
||||
eventsRecorder.assertSingle(ShowLocationEvents.DismissDialog) |
||||
} |
||||
} |
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setShowLocationView( |
||||
state: ShowLocationState, |
||||
onBackPressed: () -> Unit = EnsureNeverCalled(), |
||||
) { |
||||
setContent { |
||||
// Simulate a LocalInspectionMode for MapboxMap |
||||
CompositionLocalProvider(LocalInspectionMode provides true) { |
||||
ShowLocationView( |
||||
state = state, |
||||
onBackPressed = onBackPressed, |
||||
) |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdetails.impl.members |
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember |
||||
import io.element.android.libraries.matrix.ui.room.sortingName |
||||
import java.text.Collator |
||||
|
||||
// Comparator used to sort room members by power level (descending) and then by name (ascending) |
||||
internal class PowerLevelRoomMemberComparator : Comparator<RoomMember> { |
||||
// Used to simplify and compare unicode and ASCII chars (á == a) |
||||
private val collator = Collator.getInstance().apply { |
||||
decomposition = Collator.CANONICAL_DECOMPOSITION |
||||
} |
||||
override fun compare(o1: RoomMember, o2: RoomMember): Int { |
||||
return when { |
||||
o1.powerLevel > o2.powerLevel -> return -1 |
||||
o1.powerLevel < o2.powerLevel -> return 1 |
||||
else -> { |
||||
collator.compare(o1.sortingName(), o2.sortingName()) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,96 @@
@@ -0,0 +1,96 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdetails.impl.blockuser |
||||
|
||||
import androidx.activity.ComponentActivity |
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule |
||||
import androidx.test.ext.junit.runners.AndroidJUnit4 |
||||
import io.element.android.features.roomdetails.impl.R |
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents |
||||
import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState |
||||
import io.element.android.features.roomdetails.impl.members.details.aRoomMemberDetailsState |
||||
import io.element.android.libraries.ui.strings.CommonStrings |
||||
import io.element.android.tests.testutils.EventsRecorder |
||||
import io.element.android.tests.testutils.clickOn |
||||
import org.junit.Rule |
||||
import org.junit.Test |
||||
import org.junit.runner.RunWith |
||||
|
||||
@RunWith(AndroidJUnit4::class) |
||||
class BlockUserDialogsTest { |
||||
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>() |
||||
|
||||
@Test |
||||
fun `confirm block user emit expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() |
||||
rule.setContent { |
||||
BlockUserDialogs( |
||||
state = aRoomMemberDetailsState( |
||||
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
) |
||||
} |
||||
rule.clickOn(R.string.screen_dm_details_block_alert_action) |
||||
eventsRecorder.assertSingle(RoomMemberDetailsEvents.BlockUser(false)) |
||||
} |
||||
|
||||
@Test |
||||
fun `cancel block user emit expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() |
||||
rule.setContent { |
||||
BlockUserDialogs( |
||||
state = aRoomMemberDetailsState( |
||||
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Block, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
) |
||||
} |
||||
rule.clickOn(CommonStrings.action_cancel) |
||||
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) |
||||
} |
||||
|
||||
@Test |
||||
fun `confirm unblock user emit expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() |
||||
rule.setContent { |
||||
BlockUserDialogs( |
||||
state = aRoomMemberDetailsState( |
||||
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
) |
||||
} |
||||
rule.clickOn(R.string.screen_dm_details_unblock_alert_action) |
||||
eventsRecorder.assertSingle(RoomMemberDetailsEvents.UnblockUser(false)) |
||||
} |
||||
|
||||
@Test |
||||
fun `cancel unblock user emit expected Event`() { |
||||
val eventsRecorder = EventsRecorder<RoomMemberDetailsEvents>() |
||||
rule.setContent { |
||||
BlockUserDialogs( |
||||
state = aRoomMemberDetailsState( |
||||
displayConfirmationDialog = RoomMemberDetailsState.ConfirmationDialog.Unblock, |
||||
eventSink = eventsRecorder, |
||||
) |
||||
) |
||||
} |
||||
rule.clickOn(CommonStrings.action_cancel) |
||||
eventsRecorder.assertSingle(RoomMemberDetailsEvents.ClearConfirmationDialog) |
||||
} |
||||
} |
@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomdetails.members |
||||
|
||||
import io.element.android.features.roomdetails.impl.members.PowerLevelRoomMemberComparator |
||||
import io.element.android.features.roomdetails.impl.members.aRoomMember |
||||
import io.element.android.libraries.matrix.api.core.UserId |
||||
import io.element.android.libraries.matrix.test.A_USER_ID |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2 |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3 |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_4 |
||||
import io.element.android.libraries.matrix.test.A_USER_ID_5 |
||||
import org.junit.Test |
||||
|
||||
class PowerLevelRoomMemberComparatorTest { |
||||
@Test |
||||
fun `order is Admin, then Moderator, then User`() { |
||||
val memberList = listOf( |
||||
aRoomMember(userId = UserId("@admin:example.com"), powerLevel = 100), |
||||
aRoomMember(userId = UserId("@moderator:example.com"), powerLevel = 50), |
||||
aRoomMember(userId = UserId("@user:example.com"), powerLevel = 0), |
||||
).shuffled() |
||||
|
||||
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) |
||||
assert(ordered[0].userId == UserId("@admin:example.com")) |
||||
assert(ordered[1].userId == UserId("@moderator:example.com")) |
||||
assert(ordered[2].userId == UserId("@user:example.com")) |
||||
} |
||||
|
||||
@Test |
||||
fun `with the same power level, alphabetical ascending order for name is used`() { |
||||
val memberList = listOf( |
||||
aRoomMember(userId = A_USER_ID, displayName = "First - admin", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_2, displayName = "Second - admin", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_3, displayName = "Third - admin", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_4, displayName = "First - user", powerLevel = 0), |
||||
aRoomMember(userId = A_USER_ID_5, displayName = "Second - user", powerLevel = 0), |
||||
).shuffled() |
||||
|
||||
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) |
||||
assert(ordered[0].userId == A_USER_ID) |
||||
assert(ordered[1].userId == A_USER_ID_2) |
||||
assert(ordered[2].userId == A_USER_ID_3) |
||||
assert(ordered[3].userId == A_USER_ID_4) |
||||
assert(ordered[4].userId == A_USER_ID_5) |
||||
} |
||||
|
||||
@Test |
||||
fun `when no names are provided, alphabetical order uses user id`() { |
||||
val memberList = listOf( |
||||
aRoomMember(userId = A_USER_ID, displayName = "Z - LAST!", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_2, powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_3, powerLevel = 100), |
||||
).shuffled() |
||||
|
||||
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) |
||||
assert(ordered[0].userId == A_USER_ID_2) |
||||
assert(ordered[1].userId == A_USER_ID_3) |
||||
assert(ordered[2].userId == A_USER_ID) |
||||
} |
||||
|
||||
@Test |
||||
fun `unicode characters are simplified and compared, order ignores case`() { |
||||
val memberList = listOf( |
||||
aRoomMember(userId = A_USER_ID, displayName = "First", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_2, displayName = "Șecond", powerLevel = 100), |
||||
aRoomMember(userId = A_USER_ID_3, displayName = "third", powerLevel = 100), |
||||
).shuffled() |
||||
|
||||
val ordered = memberList.sortedWith(PowerLevelRoomMemberComparator()) |
||||
assert(ordered[0].userId == A_USER_ID) |
||||
assert(ordered[1].userId == A_USER_ID_2) |
||||
assert(ordered[2].userId == A_USER_ID_3) |
||||
} |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.di |
||||
|
||||
import com.squareup.anvil.annotations.ContributesTo |
||||
import dagger.Binds |
||||
import dagger.Module |
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchPresenter |
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.di.SessionScope |
||||
|
||||
@ContributesTo(SessionScope::class) |
||||
@Module |
||||
interface RoomListModule { |
||||
@Binds |
||||
fun bindSearchPresenter(presenter: RoomListSearchPresenter): Presenter<RoomListSearchState> |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.search |
||||
|
||||
sealed interface RoomListSearchEvents { |
||||
data object ToggleSearchVisibility : RoomListSearchEvents |
||||
data class QueryChanged(val query: String) : RoomListSearchEvents |
||||
data object ClearQuery : RoomListSearchEvents |
||||
} |
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.search |
||||
|
||||
import androidx.compose.runtime.Composable |
||||
import androidx.compose.runtime.LaunchedEffect |
||||
import androidx.compose.runtime.collectAsState |
||||
import androidx.compose.runtime.getValue |
||||
import androidx.compose.runtime.mutableStateOf |
||||
import androidx.compose.runtime.remember |
||||
import androidx.compose.runtime.rememberCoroutineScope |
||||
import androidx.compose.runtime.saveable.rememberSaveable |
||||
import androidx.compose.runtime.setValue |
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory |
||||
import io.element.android.libraries.architecture.Presenter |
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary |
||||
import io.element.android.libraries.matrix.api.roomlist.loadAllIncrementally |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
import kotlinx.collections.immutable.toPersistentList |
||||
import kotlinx.coroutines.flow.flowOn |
||||
import kotlinx.coroutines.flow.map |
||||
import javax.inject.Inject |
||||
|
||||
private const val PAGE_SIZE = 50 |
||||
|
||||
class RoomListSearchPresenter @Inject constructor( |
||||
private val roomListService: RoomListService, |
||||
private val roomSummaryFactory: RoomListRoomSummaryFactory, |
||||
private val coroutineDispatchers: CoroutineDispatchers, |
||||
) : Presenter<RoomListSearchState> { |
||||
@Composable |
||||
override fun present(): RoomListSearchState { |
||||
var isSearchActive by rememberSaveable { |
||||
mutableStateOf(false) |
||||
} |
||||
var searchQuery by rememberSaveable { |
||||
mutableStateOf("") |
||||
} |
||||
val coroutineScope = rememberCoroutineScope() |
||||
|
||||
val roomList = remember { |
||||
roomListService.createRoomList( |
||||
coroutineScope = coroutineScope, |
||||
pageSize = PAGE_SIZE, |
||||
initialFilter = RoomListFilter.all(RoomListFilter.None), |
||||
source = RoomList.Source.All, |
||||
) |
||||
} |
||||
|
||||
LaunchedEffect(Unit) { |
||||
roomList.loadAllIncrementally(this) |
||||
} |
||||
LaunchedEffect(key1 = searchQuery) { |
||||
val filter = if (searchQuery.isBlank()) { |
||||
RoomListFilter.all(RoomListFilter.None) |
||||
} else { |
||||
RoomListFilter.all(RoomListFilter.NonLeft, RoomListFilter.NormalizedMatchRoomName(searchQuery)) |
||||
} |
||||
roomList.updateFilter(filter) |
||||
} |
||||
|
||||
fun handleEvents(event: RoomListSearchEvents) { |
||||
when (event) { |
||||
RoomListSearchEvents.ClearQuery -> { |
||||
searchQuery = "" |
||||
} |
||||
is RoomListSearchEvents.QueryChanged -> { |
||||
searchQuery = event.query |
||||
} |
||||
RoomListSearchEvents.ToggleSearchVisibility -> { |
||||
isSearchActive = !isSearchActive |
||||
searchQuery = "" |
||||
} |
||||
} |
||||
} |
||||
|
||||
val searchResults by roomList |
||||
.rememberMappedSummaries() |
||||
.collectAsState(initial = persistentListOf()) |
||||
|
||||
return RoomListSearchState( |
||||
isSearchActive = isSearchActive, |
||||
query = searchQuery, |
||||
results = searchResults, |
||||
eventSink = ::handleEvents |
||||
) |
||||
} |
||||
|
||||
@Composable |
||||
private fun RoomList.rememberMappedSummaries() = remember { |
||||
summaries |
||||
.map { roomSummaries -> |
||||
roomSummaries |
||||
.filterIsInstance<RoomSummary.Filled>() |
||||
.map(roomSummaryFactory::create) |
||||
.toPersistentList() |
||||
} |
||||
.flowOn(coroutineDispatchers.computation) |
||||
} |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.search |
||||
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
|
||||
data class RoomListSearchState( |
||||
val isSearchActive: Boolean, |
||||
val query: String, |
||||
val results: ImmutableList<RoomListRoomSummary>, |
||||
val eventSink: (RoomListSearchEvents) -> Unit |
||||
) |
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.search |
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider |
||||
import io.element.android.features.roomlist.impl.aRoomListRoomSummaryList |
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary |
||||
import kotlinx.collections.immutable.ImmutableList |
||||
import kotlinx.collections.immutable.persistentListOf |
||||
|
||||
class RoomListSearchStateProvider : PreviewParameterProvider<RoomListSearchState> { |
||||
override val values: Sequence<RoomListSearchState> |
||||
get() = sequenceOf( |
||||
aRoomListSearchState(), |
||||
aRoomListSearchState( |
||||
isSearchActive = true, |
||||
query = "Test", |
||||
results = aRoomListRoomSummaryList() |
||||
), |
||||
) |
||||
} |
||||
|
||||
fun aRoomListSearchState( |
||||
isSearchActive: Boolean = false, |
||||
query: String = "", |
||||
results: ImmutableList<RoomListRoomSummary> = persistentListOf(), |
||||
eventSink: (RoomListSearchEvents) -> Unit = { }, |
||||
) = RoomListSearchState( |
||||
isSearchActive = isSearchActive, |
||||
query = query, |
||||
results = results, |
||||
eventSink = eventSink, |
||||
) |
@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.features.roomlist.impl.search |
||||
|
||||
import app.cash.molecule.RecompositionMode |
||||
import app.cash.molecule.moleculeFlow |
||||
import app.cash.turbine.test |
||||
import com.google.common.truth.Truth.assertThat |
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory |
||||
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter |
||||
import io.element.android.libraries.eventformatter.test.FakeRoomLastMessageFormatter |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListService |
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary |
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled |
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService |
||||
import io.element.android.tests.testutils.testCoroutineDispatchers |
||||
import kotlinx.coroutines.test.TestScope |
||||
import kotlinx.coroutines.test.runTest |
||||
import org.junit.Test |
||||
|
||||
class RoomListSearchPresenterTests { |
||||
@Test |
||||
fun `present - initial state`() = runTest { |
||||
val presenter = createRoomListSearchPresenter() |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
awaitItem().let { state -> |
||||
assertThat(state.isSearchActive).isFalse() |
||||
assertThat(state.query).isEmpty() |
||||
assertThat(state.results).isEmpty() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - toggle search visibility`() = runTest { |
||||
val presenter = createRoomListSearchPresenter() |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
awaitItem().let { state -> |
||||
assertThat(state.isSearchActive).isFalse() |
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) |
||||
} |
||||
awaitItem().let { state -> |
||||
assertThat(state.isSearchActive).isTrue() |
||||
state.eventSink(RoomListSearchEvents.ToggleSearchVisibility) |
||||
} |
||||
awaitItem().let { state -> |
||||
assertThat(state.isSearchActive).isFalse() |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - query search changes`() = runTest { |
||||
val roomListService = FakeRoomListService() |
||||
val presenter = createRoomListSearchPresenter(roomListService) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
awaitItem().let { state -> |
||||
assertThat( |
||||
roomListService.allRooms.currentFilter.value |
||||
).isEqualTo( |
||||
RoomListFilter.all( |
||||
RoomListFilter.None, |
||||
) |
||||
) |
||||
state.eventSink(RoomListSearchEvents.QueryChanged("Search")) |
||||
} |
||||
awaitItem().let { state -> |
||||
assertThat(state.query).isEqualTo("Search") |
||||
assertThat( |
||||
roomListService.allRooms.currentFilter.value |
||||
).isEqualTo( |
||||
RoomListFilter.all( |
||||
RoomListFilter.NonLeft, |
||||
RoomListFilter.NormalizedMatchRoomName("Search") |
||||
) |
||||
) |
||||
state.eventSink(RoomListSearchEvents.ClearQuery) |
||||
} |
||||
awaitItem().let { state -> |
||||
assertThat(state.query).isEmpty() |
||||
assertThat( |
||||
roomListService.allRooms.currentFilter.value |
||||
).isEqualTo( |
||||
RoomListFilter.all( |
||||
RoomListFilter.None, |
||||
) |
||||
) |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun `present - room list changes`() = runTest { |
||||
val roomListService = FakeRoomListService() |
||||
val presenter = createRoomListSearchPresenter(roomListService) |
||||
moleculeFlow(RecompositionMode.Immediate) { |
||||
presenter.present() |
||||
}.test { |
||||
awaitItem().let { state -> |
||||
assertThat(state.results).isEmpty() |
||||
} |
||||
roomListService.postAllRooms( |
||||
listOf( |
||||
RoomSummary.Empty("1"), |
||||
aRoomSummaryFilled() |
||||
) |
||||
) |
||||
awaitItem().let { state -> |
||||
assertThat(state.results).hasSize(1) |
||||
} |
||||
roomListService.postAllRooms(emptyList()) |
||||
awaitItem().let { state -> |
||||
assertThat(state.results).isEmpty() |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
fun TestScope.createRoomListSearchPresenter( |
||||
roomListService: RoomListService = FakeRoomListService(), |
||||
): RoomListSearchPresenter { |
||||
return RoomListSearchPresenter( |
||||
roomListService = roomListService, |
||||
roomSummaryFactory = RoomListRoomSummaryFactory( |
||||
lastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(), |
||||
roomLastMessageFormatter = FakeRoomLastMessageFormatter(), |
||||
), |
||||
coroutineDispatchers = testCoroutineDispatchers(), |
||||
) |
||||
} |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
/* |
||||
* Copyright (c) 2024 New Vector Ltd |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* http://www.apache.org/licenses/LICENSE-2.0 |
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package io.element.android.libraries.matrix.ui.room |
||||
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember |
||||
|
||||
/** |
||||
* Returns the name value to use when sorting room members. |
||||
* |
||||
* If the display name is not null and not empty, it is returned. |
||||
* Otherwise, the user ID is returned without the initial "@". |
||||
*/ |
||||
fun RoomMember.sortingName(): String { |
||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value.drop(1) |
||||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue