Merge pull request #2714 from element-hq/feature/fga/room_list_invites
[Feature] Room list invites
This commit is contained in:
@@ -45,11 +45,4 @@ class IntentProviderImpl @Inject constructor(
|
||||
data = deepLinkCreator.room(sessionId, roomId, threadId).toUri()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getInviteListIntent(sessionId: SessionId): Intent {
|
||||
return Intent(context, MainActivity::class.java).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
data = deepLinkCreator.inviteList(sessionId).toUri()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,16 +67,6 @@ class IntentProviderImplTest {
|
||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `test getInviteListIntent`() {
|
||||
val sut = createIntentProviderImpl()
|
||||
val result = sut.getInviteListIntent(
|
||||
sessionId = A_SESSION_ID,
|
||||
)
|
||||
result.commonAssertions()
|
||||
assertThat(result.data.toString()).isEqualTo("elementx://open/@alice:server.org/invites")
|
||||
}
|
||||
|
||||
private fun createIntentProviderImpl(): IntentProviderImpl {
|
||||
return IntentProviderImpl(
|
||||
context = RuntimeEnvironment.getApplication() as Context,
|
||||
|
||||
@@ -33,7 +33,6 @@ import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
||||
import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
||||
@@ -48,7 +47,6 @@ import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
||||
import io.element.android.features.ftue.api.FtueEntryPoint
|
||||
import io.element.android.features.ftue.api.state.FtueService
|
||||
import io.element.android.features.ftue.api.state.FtueState
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
@@ -62,17 +60,13 @@ import io.element.android.features.securebackup.api.SecureBackupEntryPoint
|
||||
import io.element.android.libraries.architecture.BackstackView
|
||||
import io.element.android.libraries.architecture.BaseFlowNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.architecture.waitForChildAttached
|
||||
import io.element.android.libraries.deeplink.DeeplinkData
|
||||
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.MAIN_SPACE
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
import io.element.android.libraries.push.api.notifications.NotificationDrawerManager
|
||||
import io.element.android.services.appnavstate.api.AppNavigationStateService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
@@ -81,7 +75,6 @@ import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import timber.log.Timber
|
||||
import java.util.Optional
|
||||
@@ -95,11 +88,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||
private val appNavigationStateService: AppNavigationStateService,
|
||||
private val secureBackupEntryPoint: SecureBackupEntryPoint,
|
||||
private val inviteListEntryPoint: InviteListEntryPoint,
|
||||
private val ftueEntryPoint: FtueEntryPoint,
|
||||
private val coroutineScope: CoroutineScope,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val ftueService: FtueService,
|
||||
private val lockScreenEntryPoint: LockScreenEntryPoint,
|
||||
private val lockScreenStateService: LockScreenService,
|
||||
@@ -160,23 +151,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
)
|
||||
observeSyncStateAndNetworkStatus()
|
||||
observeInvitesLoadingState()
|
||||
}
|
||||
|
||||
private fun observeInvitesLoadingState() {
|
||||
lifecycleScope.launch {
|
||||
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
matrixClient.roomListService.invites.loadingState
|
||||
.collect { inviteState ->
|
||||
when (inviteState) {
|
||||
is RoomList.LoadingState.Loaded -> if (inviteState.numberOfRooms == 0) {
|
||||
backstack.removeLast(NavTarget.InviteList)
|
||||
}
|
||||
RoomList.LoadingState.NotLoaded -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@@ -233,9 +207,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
val initialElement: SecureBackupEntryPoint.InitialTarget = SecureBackupEntryPoint.InitialTarget.Root
|
||||
) : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object InviteList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object Ftue : NavTarget
|
||||
|
||||
@@ -272,10 +243,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
backstack.push(NavTarget.SecureBackup(initialElement = SecureBackupEntryPoint.InitialTarget.EnterRecoveryKey))
|
||||
}
|
||||
|
||||
override fun onInvitesClicked() {
|
||||
backstack.push(NavTarget.InviteList)
|
||||
}
|
||||
|
||||
override fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId, initialElement = RoomNavigationTarget.Details))
|
||||
}
|
||||
@@ -351,25 +318,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
.params(SecureBackupEntryPoint.Params(initialElement = navTarget.initialElement))
|
||||
.build()
|
||||
}
|
||||
NavTarget.InviteList -> {
|
||||
val callback = object : InviteListEntryPoint.Callback {
|
||||
override fun onBackClicked() {
|
||||
backstack.pop()
|
||||
}
|
||||
|
||||
override fun onInviteClicked(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
|
||||
override fun onInviteAccepted(roomId: RoomId) {
|
||||
backstack.push(NavTarget.Room(roomId))
|
||||
}
|
||||
}
|
||||
|
||||
inviteListEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
NavTarget.Ftue -> {
|
||||
ftueEntryPoint.nodeBuilder(this, buildContext)
|
||||
.callback(object : FtueEntryPoint.Callback {
|
||||
@@ -410,16 +358,6 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun attachInviteList(deeplinkData: DeeplinkData.InviteList) = withContext(lifecycleScope.coroutineContext) {
|
||||
if (!canShowRoomList()) return@withContext
|
||||
notificationDrawerManager.clearMembershipNotificationForSession(deeplinkData.sessionId)
|
||||
backstack.singleTop(NavTarget.RoomList)
|
||||
backstack.push(NavTarget.InviteList)
|
||||
waitForChildAttached<Node, NavTarget> { navTarget ->
|
||||
navTarget is NavTarget.InviteList
|
||||
}
|
||||
}
|
||||
|
||||
private fun canShowRoomList(): Boolean {
|
||||
return ftueService.state.value is FtueState.Complete
|
||||
}
|
||||
|
||||
@@ -290,7 +290,6 @@ class RootFlowNode @AssistedInject constructor(
|
||||
when (deeplinkData) {
|
||||
is DeeplinkData.Root -> attachRoomList()
|
||||
is DeeplinkData.Room -> attachRoom(deeplinkData.roomId)
|
||||
is DeeplinkData.InviteList -> attachInviteList(deeplinkData)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.api
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import io.element.android.libraries.architecture.FeatureEntryPoint
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
interface InviteListEntryPoint : FeatureEntryPoint {
|
||||
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
|
||||
|
||||
interface NodeBuilder {
|
||||
fun callback(callback: Callback): NodeBuilder
|
||||
fun build(): Node
|
||||
}
|
||||
|
||||
interface Callback : Plugin {
|
||||
fun onBackClicked()
|
||||
fun onInviteClicked(roomId: RoomId)
|
||||
fun onInviteAccepted(roomId: RoomId)
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.api
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SeenInvitesStore {
|
||||
fun seenRoomIds(): Flow<Set<RoomId>>
|
||||
suspend fun markAsSeen(roomIds: Set<RoomId>)
|
||||
}
|
||||
@@ -50,7 +50,6 @@ dependencies {
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.libraries.push.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.features.invite.impl.invitelist.InviteListNode
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultInviteListEntryPoint @Inject constructor() : InviteListEntryPoint {
|
||||
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): InviteListEntryPoint.NodeBuilder {
|
||||
val plugins = ArrayList<Plugin>()
|
||||
|
||||
return object : InviteListEntryPoint.NodeBuilder {
|
||||
override fun callback(callback: InviteListEntryPoint.Callback): InviteListEntryPoint.NodeBuilder {
|
||||
plugins += callback
|
||||
return this
|
||||
}
|
||||
|
||||
override fun build(): Node {
|
||||
return parentNode.createNode<InviteListNode>(buildContext, plugins)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringSetPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import javax.inject.Inject
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "elementx_seeninvites")
|
||||
private val seenInvitesKey = stringSetPreferencesKey("seenInvites")
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultSeenInvitesStore @Inject constructor(
|
||||
@ApplicationContext context: Context
|
||||
) : SeenInvitesStore {
|
||||
private val store = context.dataStore
|
||||
|
||||
override fun seenRoomIds(): Flow<Set<RoomId>> =
|
||||
store.data.map { prefs ->
|
||||
prefs[seenInvitesKey]
|
||||
.orEmpty()
|
||||
.map { RoomId(it) }
|
||||
.toSet()
|
||||
}
|
||||
|
||||
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
||||
store.edit { prefs ->
|
||||
prefs[seenInvitesKey] = roomIds.map { it.value }.toSet()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,195 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invite.impl.R
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummaryProvider
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
private val minHeight = 72.dp
|
||||
|
||||
@Composable
|
||||
internal fun InviteSummaryRow(
|
||||
invite: InviteListInviteSummary,
|
||||
onAcceptClicked: () -> Unit,
|
||||
onDeclineClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
) {
|
||||
DefaultInviteSummaryRow(
|
||||
invite = invite,
|
||||
onAcceptClicked = onAcceptClicked,
|
||||
onDeclineClicked = onDeclineClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DefaultInviteSummaryRow(
|
||||
invite: InviteListInviteSummary,
|
||||
onAcceptClicked: () -> Unit,
|
||||
onDeclineClicked: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
verticalAlignment = Alignment.Top
|
||||
) {
|
||||
Avatar(
|
||||
invite.roomAvatarData,
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = 16.dp, end = 4.dp)
|
||||
.alignByBaseline()
|
||||
.weight(1f)
|
||||
) {
|
||||
val bonusPadding = if (invite.isNew) 12.dp else 0.dp
|
||||
|
||||
// Name
|
||||
Text(
|
||||
text = invite.roomName,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
modifier = Modifier.padding(end = bonusPadding),
|
||||
)
|
||||
|
||||
// ID or Alias
|
||||
invite.roomAlias?.let {
|
||||
Text(
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(end = bonusPadding),
|
||||
)
|
||||
}
|
||||
|
||||
// Sender
|
||||
invite.sender?.let { sender ->
|
||||
SenderRow(sender = sender)
|
||||
}
|
||||
|
||||
// CTAs
|
||||
Row(Modifier.padding(top = 12.dp)) {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.Medium,
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptClicked,
|
||||
modifier = Modifier.weight(1f),
|
||||
size = ButtonSize.Medium,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
UnreadIndicatorAtom(isVisible = invite.isNew)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SenderRow(sender: InviteSender) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.padding(top = 6.dp),
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = sender.avatarData,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.screen_invites_invited_you, sender.displayName, sender.userId.value).let { text ->
|
||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
||||
AnnotatedString(
|
||||
text = text,
|
||||
spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
start = senderNameStart,
|
||||
end = senderNameStart + sender.displayName.length
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun InviteSummaryRowPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) = ElementPreview {
|
||||
InviteSummaryRow(
|
||||
invite = data,
|
||||
onAcceptClicked = {},
|
||||
onDeclineClicked = {},
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
|
||||
sealed interface InviteListEvents {
|
||||
data class AcceptInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
data class DeclineInvite(val invite: InviteListInviteSummary) : InviteListEvents
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.bumble.appyx.core.plugin.Plugin
|
||||
import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.invite.api.InviteListEntryPoint
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
class InviteListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: InviteListPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
private fun onBackClicked() {
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onBackClicked() }
|
||||
}
|
||||
|
||||
private fun onInviteAccepted(roomId: RoomId) {
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteAccepted(roomId) }
|
||||
}
|
||||
|
||||
private fun onInviteClicked(roomId: RoomId) {
|
||||
plugins<InviteListEntryPoint.Callback>().forEach { it.onInviteClicked(roomId) }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
InviteListView(
|
||||
state = state,
|
||||
onBackClicked = ::onBackClicked,
|
||||
onInviteAccepted = ::onInviteAccepted,
|
||||
onInviteDeclined = {},
|
||||
onInviteClicked = ::onInviteClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
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.setValue
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.collections.immutable.toPersistentList
|
||||
import kotlinx.coroutines.flow.first
|
||||
import javax.inject.Inject
|
||||
|
||||
class InviteListPresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val store: SeenInvitesStore,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
) : Presenter<InviteListState> {
|
||||
@Composable
|
||||
override fun present(): InviteListState {
|
||||
val invites by client
|
||||
.roomListService
|
||||
.invites
|
||||
.summaries
|
||||
.collectAsState(initial = emptyList())
|
||||
|
||||
var seenInvites by remember { mutableStateOf<Set<RoomId>>(emptySet()) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
seenInvites = store.seenRoomIds().first()
|
||||
}
|
||||
|
||||
LaunchedEffect(invites) {
|
||||
store.markAsSeen(
|
||||
invites
|
||||
.filterIsInstance<RoomSummary.Filled>()
|
||||
.map { it.details.roomId }
|
||||
.toSet()
|
||||
)
|
||||
}
|
||||
|
||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||
|
||||
fun handleEvent(event: InviteListEvents) {
|
||||
when (event) {
|
||||
is InviteListEvents.AcceptInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.AcceptInvite(event.invite.toInviteData())
|
||||
)
|
||||
}
|
||||
|
||||
is InviteListEvents.DeclineInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(event.invite.toInviteData())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val inviteList = remember(seenInvites, invites) {
|
||||
invites
|
||||
.filterIsInstance<RoomSummary.Filled>()
|
||||
.map {
|
||||
it.toInviteSummary(seenInvites.contains(it.details.roomId))
|
||||
}
|
||||
.toPersistentList()
|
||||
}
|
||||
|
||||
return InviteListState(
|
||||
inviteList = inviteList,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
}
|
||||
|
||||
private fun RoomSummary.Filled.toInviteSummary(seen: Boolean) = details.run {
|
||||
val i = inviter
|
||||
val avatarData = if (isDirect && i != null) {
|
||||
AvatarData(
|
||||
id = i.userId.value,
|
||||
name = i.displayName,
|
||||
url = i.avatarUrl,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
} else {
|
||||
AvatarData(
|
||||
id = roomId.value,
|
||||
name = name,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
}
|
||||
|
||||
val alias = if (isDirect) {
|
||||
inviter?.userId?.value
|
||||
} else {
|
||||
canonicalAlias
|
||||
}
|
||||
|
||||
InviteListInviteSummary(
|
||||
roomId = roomId,
|
||||
roomName = name,
|
||||
roomAlias = alias,
|
||||
roomAvatarData = avatarData,
|
||||
isDirect = isDirect,
|
||||
isNew = !seen,
|
||||
sender = inviter
|
||||
?.takeIf { !isDirect }
|
||||
?.run {
|
||||
InviteSender(
|
||||
userId = userId,
|
||||
displayName = displayName ?: "",
|
||||
avatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.InviteSender,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun InviteListInviteSummary.toInviteData() = InviteData(
|
||||
roomId = roomId,
|
||||
roomName = roomName,
|
||||
isDirect = isDirect,
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class InviteListState(
|
||||
val inviteList: ImmutableList<InviteListInviteSummary>,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
val eventSink: (InviteListEvents) -> Unit
|
||||
)
|
||||
@@ -1,77 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteStateProvider
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.impl.model.InviteListInviteSummary
|
||||
import io.element.android.features.invite.impl.model.InviteSender
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class InviteListStateProvider : PreviewParameterProvider<InviteListState> {
|
||||
private val acceptDeclineInviteStateProvider = AcceptDeclineInviteStateProvider()
|
||||
|
||||
override val values: Sequence<InviteListState>
|
||||
get() = sequenceOf(
|
||||
anInviteListState(),
|
||||
anInviteListState(inviteList = persistentListOf()),
|
||||
) + acceptDeclineInviteStateProvider.values.map { acceptDeclineInviteState ->
|
||||
anInviteListState(acceptDeclineInviteState = acceptDeclineInviteState)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun anInviteListState(
|
||||
inviteList: ImmutableList<InviteListInviteSummary> = aInviteListInviteSummaryList(),
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
eventSink: (InviteListEvents) -> Unit = {}
|
||||
) = InviteListState(
|
||||
inviteList = inviteList,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
|
||||
return persistentListOf(
|
||||
InviteListInviteSummary(
|
||||
roomId = RoomId("!id1:example.com"),
|
||||
roomName = "Room 1",
|
||||
roomAlias = "#room:example.org",
|
||||
sender = InviteSender(
|
||||
userId = UserId("@alice:example.org"),
|
||||
displayName = "Alice"
|
||||
),
|
||||
),
|
||||
InviteListInviteSummary(
|
||||
roomId = RoomId("!id2:example.com"),
|
||||
roomName = "Room 2",
|
||||
sender = InviteSender(
|
||||
userId = UserId("@bob:example.org"),
|
||||
displayName = "Bob"
|
||||
),
|
||||
),
|
||||
InviteListInviteSummary(
|
||||
roomId = RoomId("!id3:example.com"),
|
||||
roomName = "Alice",
|
||||
roomAlias = "@alice:example.com"
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
/*
|
||||
* 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.invite.impl.invitelist
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.invite.impl.R
|
||||
import io.element.android.features.invite.impl.components.InviteSummaryRow
|
||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun InviteListView(
|
||||
state: InviteListState,
|
||||
onBackClicked: () -> Unit,
|
||||
onInviteAccepted: (RoomId) -> Unit,
|
||||
onInviteDeclined: (RoomId) -> Unit,
|
||||
onInviteClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
InviteListContent(
|
||||
state = state,
|
||||
modifier = modifier,
|
||||
onInviteClicked = onInviteClicked,
|
||||
onBackClicked = onBackClicked,
|
||||
)
|
||||
AcceptDeclineInviteView(
|
||||
state = state.acceptDeclineInviteState,
|
||||
onInviteAccepted = onInviteAccepted,
|
||||
onInviteDeclined = onInviteDeclined,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun InviteListContent(
|
||||
state: InviteListState,
|
||||
onBackClicked: () -> Unit,
|
||||
onInviteClicked: (RoomId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
BackButton(onClick = onBackClicked)
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
content = { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
) {
|
||||
if (state.inviteList.isEmpty()) {
|
||||
Spacer(Modifier.size(80.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.screen_invites_empty_list),
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.tertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
itemsIndexed(
|
||||
items = state.inviteList,
|
||||
) { index, invite ->
|
||||
InviteSummaryRow(
|
||||
modifier = Modifier.clickable(
|
||||
onClick = { onInviteClicked(invite.roomId) }
|
||||
),
|
||||
invite = invite,
|
||||
onAcceptClicked = { state.eventSink(InviteListEvents.AcceptInvite(invite)) },
|
||||
onDeclineClicked = { state.eventSink(InviteListEvents.DeclineInvite(invite)) },
|
||||
)
|
||||
|
||||
if (index != state.inviteList.lastIndex) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun InviteListViewPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) = ElementPreview {
|
||||
InviteListView(
|
||||
state = state,
|
||||
onBackClicked = {},
|
||||
onInviteAccepted = {},
|
||||
onInviteDeclined = {},
|
||||
onInviteClicked = {},
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@Immutable
|
||||
data class InviteListInviteSummary(
|
||||
val roomId: RoomId,
|
||||
val roomName: String = "",
|
||||
val roomAlias: String? = null,
|
||||
val roomAvatarData: AvatarData = AvatarData(roomId.value, roomName, size = AvatarSize.RoomInviteItem),
|
||||
val sender: InviteSender? = null,
|
||||
val isDirect: Boolean = false,
|
||||
val isNew: Boolean = false,
|
||||
)
|
||||
|
||||
data class InviteSender(
|
||||
val userId: UserId,
|
||||
val displayName: String,
|
||||
val avatarData: AvatarData = AvatarData(userId.value, displayName, size = AvatarSize.InviteSender),
|
||||
)
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl.model
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
open class InviteListInviteSummaryProvider : PreviewParameterProvider<InviteListInviteSummary> {
|
||||
override val values: Sequence<InviteListInviteSummary>
|
||||
get() = sequenceOf(
|
||||
aInviteListInviteSummary(),
|
||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com"),
|
||||
aInviteListInviteSummary().copy(roomAlias = "#someroom-with-a-long-alias:example.com", isNew = true),
|
||||
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
|
||||
aInviteListInviteSummary().copy(isNew = true)
|
||||
)
|
||||
}
|
||||
|
||||
fun aInviteListInviteSummary() = InviteListInviteSummary(
|
||||
roomId = RoomId("!room1:example.com"),
|
||||
roomName = "Some room with a long name that will truncate",
|
||||
sender = InviteSender(
|
||||
userId = UserId("@alice-with-a-long-mxid:example.org"),
|
||||
displayName = "Alice with a long name"
|
||||
),
|
||||
)
|
||||
@@ -1,266 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.impl.invitelist
|
||||
|
||||
import app.cash.molecule.RecompositionMode
|
||||
import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.TurbineTestContext
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.invite.test.FakeSeenInvitesStore
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipState
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_NAME
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.room.aRoomMember
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
||||
class InviteListPresenterTests {
|
||||
@get:Rule
|
||||
val warmUpRule = WarmUpRule()
|
||||
|
||||
@Test
|
||||
fun `present - starts empty, adds invites when received`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val presenter = createInviteListPresenter(
|
||||
FakeMatrixClient(roomListService = roomListService)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val initialState = awaitItem()
|
||||
assertThat(initialState.inviteList).isEmpty()
|
||||
|
||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
||||
|
||||
val withInviteState = awaitItem()
|
||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - uses user ID and avatar for direct invites`() = runTest {
|
||||
val roomListService = FakeRoomListService().withDirectChatInvitation()
|
||||
val presenter = createInviteListPresenter(
|
||||
FakeMatrixClient(roomListService = roomListService)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val withInviteState = awaitInitialItem()
|
||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
|
||||
assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
|
||||
assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
|
||||
AvatarData(
|
||||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.RoomInviteItem,
|
||||
)
|
||||
)
|
||||
assertThat(withInviteState.inviteList[0].sender).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - includes sender details for room invites`() = runTest {
|
||||
val roomListService = FakeRoomListService().withRoomInvitation()
|
||||
val presenter = createInviteListPresenter(
|
||||
FakeMatrixClient(roomListService = roomListService)
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val withInviteState = awaitInitialItem()
|
||||
assertThat(withInviteState.inviteList.size).isEqualTo(1)
|
||||
assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
|
||||
assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
|
||||
assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
|
||||
AvatarData(
|
||||
id = A_USER_ID.value,
|
||||
name = A_USER_NAME,
|
||||
url = AN_AVATAR_URL,
|
||||
size = AvatarSize.InviteSender,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - stores seen invites when received`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val store = FakeSeenInvitesStore()
|
||||
val presenter = createInviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
),
|
||||
store,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem()
|
||||
|
||||
// When one invite is received, that ID is saved
|
||||
roomListService.postInviteRooms(listOf(aRoomSummary()))
|
||||
|
||||
awaitItem()
|
||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID))
|
||||
|
||||
// When a second is added, both are saved
|
||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||
|
||||
awaitItem()
|
||||
assertThat(store.getProvidedRoomIds()).isEqualTo(setOf(A_ROOM_ID, A_ROOM_ID_2))
|
||||
|
||||
// When they're both dismissed, an empty set is saved
|
||||
roomListService.postInviteRooms(listOf())
|
||||
|
||||
awaitItem()
|
||||
assertThat(store.getProvidedRoomIds()).isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - marks invite as new if they're unseen`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val store = FakeSeenInvitesStore()
|
||||
store.publishRoomIds(setOf(A_ROOM_ID))
|
||||
val presenter = createInviteListPresenter(
|
||||
FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
),
|
||||
store,
|
||||
)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
awaitItem()
|
||||
|
||||
roomListService.postInviteRooms(listOf(aRoomSummary(), aRoomSummary(A_ROOM_ID_2)))
|
||||
skipItems(1)
|
||||
|
||||
val withInviteState = awaitItem()
|
||||
assertThat(withInviteState.inviteList.size).isEqualTo(2)
|
||||
assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
|
||||
assertThat(withInviteState.inviteList[0].isNew).isFalse()
|
||||
assertThat(withInviteState.inviteList[1].roomId).isEqualTo(A_ROOM_ID_2)
|
||||
assertThat(withInviteState.inviteList[1].isNew).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun FakeRoomListService.withRoomInvitation(): FakeRoomListService {
|
||||
postInviteRooms(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
aRoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
inviter = aRoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private suspend fun FakeRoomListService.withDirectChatInvitation(): FakeRoomListService {
|
||||
postInviteRooms(
|
||||
listOf(
|
||||
RoomSummary.Filled(
|
||||
aRoomSummaryDetails(
|
||||
roomId = A_ROOM_ID,
|
||||
name = A_ROOM_NAME,
|
||||
avatarUrl = null,
|
||||
isDirect = true,
|
||||
lastMessage = null,
|
||||
inviter = aRoomMember(
|
||||
userId = A_USER_ID,
|
||||
displayName = A_USER_NAME,
|
||||
avatarUrl = AN_AVATAR_URL,
|
||||
membership = RoomMembershipState.JOIN,
|
||||
isNameAmbiguous = false,
|
||||
powerLevel = 0,
|
||||
normalizedPowerLevel = 0,
|
||||
isIgnored = false,
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
return this
|
||||
}
|
||||
|
||||
private fun aRoomSummary(id: RoomId = A_ROOM_ID) = RoomSummary.Filled(
|
||||
aRoomSummaryDetails(
|
||||
roomId = id,
|
||||
name = A_ROOM_NAME,
|
||||
avatarUrl = null,
|
||||
isDirect = false,
|
||||
lastMessage = null,
|
||||
)
|
||||
)
|
||||
|
||||
private suspend fun TurbineTestContext<InviteListState>.awaitInitialItem(): InviteListState {
|
||||
skipItems(1)
|
||||
return awaitItem()
|
||||
}
|
||||
|
||||
private fun createInviteListPresenter(
|
||||
client: MatrixClient,
|
||||
seenInvitesStore: SeenInvitesStore = FakeSeenInvitesStore(),
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
) = InviteListPresenter(
|
||||
client,
|
||||
seenInvitesStore,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022 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.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.invite.test"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
api(projects.features.invite.api)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.invite.test
|
||||
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FakeSeenInvitesStore : SeenInvitesStore {
|
||||
private val existing = MutableStateFlow(emptySet<RoomId>())
|
||||
private var provided: Set<RoomId>? = null
|
||||
|
||||
fun publishRoomIds(invites: Set<RoomId>) {
|
||||
existing.value = invites
|
||||
}
|
||||
|
||||
fun getProvidedRoomIds() = provided
|
||||
|
||||
override fun seenRoomIds(): Flow<Set<RoomId>> = existing
|
||||
|
||||
override suspend fun markAsSeen(roomIds: Set<RoomId>) {
|
||||
provided = roomIds.toSet()
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ dependencies {
|
||||
testImplementation(libs.test.truth)
|
||||
testImplementation(libs.test.turbine)
|
||||
testImplementation(projects.libraries.matrix.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
ksp(libs.showkase.processor)
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.joinroom.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -31,7 +32,6 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoomInfo
|
||||
import org.jetbrains.annotations.VisibleForTesting
|
||||
import java.util.Optional
|
||||
|
||||
class JoinRoomPresenter @AssistedInject constructor(
|
||||
|
||||
@@ -34,7 +34,6 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||
fun onCreateRoomClicked()
|
||||
fun onSettingsClicked()
|
||||
fun onSessionConfirmRecoveryKeyClicked()
|
||||
fun onInvitesClicked()
|
||||
fun onRoomSettingsClicked(roomId: RoomId)
|
||||
fun onReportBugClicked()
|
||||
fun onRoomDirectorySearchClicked()
|
||||
|
||||
@@ -75,7 +75,6 @@ dependencies {
|
||||
testImplementation(projects.libraries.indicator.impl)
|
||||
testImplementation(projects.libraries.permissions.noop)
|
||||
testImplementation(projects.libraries.preferences.test)
|
||||
testImplementation(projects.features.invite.test)
|
||||
testImplementation(projects.services.analytics.test)
|
||||
testImplementation(projects.features.networkmonitor.test)
|
||||
testImplementation(projects.tests.testutils)
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun InvitesEntryPointView(
|
||||
onInvitesClicked: () -> Unit,
|
||||
state: InvitesState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable(role = Role.Button, onClick = onInvitesClicked)
|
||||
.padding(start = 24.dp, end = 16.dp)
|
||||
.align(Alignment.CenterEnd)
|
||||
.heightIn(min = 40.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(CommonStrings.action_invites_list),
|
||||
style = ElementTheme.typography.fontBodyMdMedium,
|
||||
)
|
||||
|
||||
if (state == InvitesState.NewInvites) {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
UnreadIndicatorAtom()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun InvitesEntryPointViewPreview(@PreviewParameter(InvitesStateProvider::class) state: InvitesState) = ElementPreview {
|
||||
InvitesEntryPointView(
|
||||
onInvitesClicked = {},
|
||||
state = state,
|
||||
)
|
||||
}
|
||||
@@ -33,11 +33,9 @@ open class RoomListContentStateProvider : PreviewParameterProvider<RoomListConte
|
||||
}
|
||||
|
||||
internal fun aRoomsContentState(
|
||||
invitesState: InvitesState = InvitesState.NoInvites,
|
||||
securityBannerState: SecurityBannerState = SecurityBannerState.None,
|
||||
summaries: ImmutableList<RoomListRoomSummary> = aRoomListRoomSummaryList(),
|
||||
) = RoomListContentState.Rooms(
|
||||
invitesState = invitesState,
|
||||
securityBannerState = securityBannerState,
|
||||
summaries = summaries,
|
||||
)
|
||||
@@ -46,6 +44,4 @@ internal fun aMigrationContentState() = RoomListContentState.Migration
|
||||
|
||||
internal fun aSkeletonContentState() = RoomListContentState.Skeleton(16)
|
||||
|
||||
internal fun anEmptyContentState(
|
||||
invitesState: InvitesState = InvitesState.NoInvites,
|
||||
) = RoomListContentState.Empty(invitesState)
|
||||
internal fun anEmptyContentState() = RoomListContentState.Empty
|
||||
|
||||
@@ -24,6 +24,8 @@ sealed interface RoomListEvents {
|
||||
data object DismissRequestVerificationPrompt : RoomListEvents
|
||||
data object DismissRecoveryKeyPrompt : RoomListEvents
|
||||
data object ToggleSearchResults : RoomListEvents
|
||||
data class AcceptInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||
data class DeclineInvite(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||
data class ShowContextMenu(val roomListRoomSummary: RoomListRoomSummary) : RoomListEvents
|
||||
|
||||
sealed interface ContextMenuEvents : RoomListEvents
|
||||
|
||||
@@ -29,6 +29,7 @@ import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import im.vector.app.features.analytics.plan.MobileScreen
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteView
|
||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.libraries.deeplink.usecase.InviteFriendsUseCase
|
||||
@@ -43,6 +44,7 @@ class RoomListNode @AssistedInject constructor(
|
||||
private val presenter: RoomListPresenter,
|
||||
private val inviteFriendsUseCase: InviteFriendsUseCase,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val acceptDeclineInviteView: AcceptDeclineInviteView,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
init {
|
||||
lifecycle.subscribe(
|
||||
@@ -68,10 +70,6 @@ class RoomListNode @AssistedInject constructor(
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionConfirmRecoveryKeyClicked() }
|
||||
}
|
||||
|
||||
private fun onInvitesClicked() {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
||||
}
|
||||
|
||||
private fun onRoomSettingsClicked(roomId: RoomId) {
|
||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onRoomSettingsClicked(roomId) }
|
||||
}
|
||||
@@ -101,11 +99,17 @@ class RoomListNode @AssistedInject constructor(
|
||||
onSettingsClicked = this::onOpenSettings,
|
||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||
onConfirmRecoveryKeyClicked = this::onSessionConfirmRecoveryKeyClicked,
|
||||
onInvitesClicked = this::onInvitesClicked,
|
||||
onRoomSettingsClicked = this::onRoomSettingsClicked,
|
||||
onMenuActionClicked = { onMenuActionClicked(activity, it) },
|
||||
onRoomDirectorySearchClicked = this::onRoomDirectorySearchClicked,
|
||||
modifier = modifier,
|
||||
)
|
||||
) {
|
||||
acceptDeclineInviteView.Render(
|
||||
state = state.acceptDeclineInviteState,
|
||||
onInviteAccepted = this::onRoomClicked,
|
||||
onInviteDeclined = { },
|
||||
modifier = Modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
@@ -32,15 +33,18 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.InviteData
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.api.NetworkStatus
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.migration.MigrationScreenState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchEvents
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
@@ -79,7 +83,6 @@ class RoomListPresenter @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val snackbarDispatcher: SnackbarDispatcher,
|
||||
private val inviteStateDataSource: InviteStateDataSource,
|
||||
private val leaveRoomPresenter: LeaveRoomPresenter,
|
||||
private val roomListDataSource: RoomListDataSource,
|
||||
private val featureFlagService: FeatureFlagService,
|
||||
@@ -89,6 +92,7 @@ class RoomListPresenter @Inject constructor(
|
||||
private val migrationScreenPresenter: Presenter<MigrationScreenState>,
|
||||
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState>,
|
||||
) : Presenter<RoomListState> {
|
||||
private val encryptionService: EncryptionService = client.encryptionService()
|
||||
private val syncService: SyncService = client.syncService()
|
||||
@@ -101,6 +105,7 @@ class RoomListPresenter @Inject constructor(
|
||||
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
|
||||
val filtersState = filtersPresenter.present()
|
||||
val searchState = searchPresenter.present()
|
||||
val acceptDeclineInviteState = acceptDeclineInvitePresenter.present()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
roomListDataSource.launchIn(this)
|
||||
@@ -131,6 +136,16 @@ class RoomListPresenter @Inject constructor(
|
||||
is RoomListEvents.SetRoomIsFavorite -> coroutineScope.setRoomIsFavorite(event.roomId, event.isFavorite)
|
||||
is RoomListEvents.MarkAsRead -> coroutineScope.markAsRead(event.roomId)
|
||||
is RoomListEvents.MarkAsUnread -> coroutineScope.markAsUnread(event.roomId)
|
||||
is RoomListEvents.AcceptInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.AcceptInvite(event.roomListRoomSummary.toInviteData())
|
||||
)
|
||||
}
|
||||
is RoomListEvents.DeclineInvite -> {
|
||||
acceptDeclineInviteState.eventSink(
|
||||
AcceptDeclineInviteEvents.DeclineInvite(event.roomListRoomSummary.toInviteData())
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +163,7 @@ class RoomListPresenter @Inject constructor(
|
||||
filtersState = filtersState,
|
||||
searchState = searchState,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
@@ -192,16 +208,11 @@ class RoomListPresenter @Inject constructor(
|
||||
}
|
||||
return when {
|
||||
showMigration -> RoomListContentState.Migration
|
||||
showEmpty -> {
|
||||
val invitesState = inviteStateDataSource.inviteState()
|
||||
RoomListContentState.Empty(invitesState)
|
||||
}
|
||||
showEmpty -> RoomListContentState.Empty
|
||||
showSkeleton -> RoomListContentState.Skeleton(count = 16)
|
||||
else -> {
|
||||
val invitesState = inviteStateDataSource.inviteState()
|
||||
val securityBannerState by securityBannerState(securityBannerDismissed)
|
||||
RoomListContentState.Rooms(
|
||||
invitesState = invitesState,
|
||||
securityBannerState = securityBannerState,
|
||||
summaries = roomSummaries.dataOrNull().orEmpty().toPersistentList()
|
||||
)
|
||||
@@ -283,3 +294,10 @@ class RoomListPresenter @Inject constructor(
|
||||
client.roomListService.updateAllRoomsVisibleRange(extendedRange)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun RoomListRoomSummary.toInviteData() = InviteData(
|
||||
roomId = roomId,
|
||||
roomName = name,
|
||||
isDirect = isDirect,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
@@ -37,6 +38,7 @@ data class RoomListState(
|
||||
val filtersState: RoomListFiltersState,
|
||||
val searchState: RoomListSearchState,
|
||||
val contentState: RoomListContentState,
|
||||
val acceptDeclineInviteState: AcceptDeclineInviteState,
|
||||
val eventSink: (RoomListEvents) -> Unit,
|
||||
) {
|
||||
val displayFilters = filtersState.isFeatureEnabled && contentState is RoomListContentState.Rooms
|
||||
@@ -70,9 +72,8 @@ enum class SecurityBannerState {
|
||||
sealed interface RoomListContentState {
|
||||
data object Migration : RoomListContentState
|
||||
data class Skeleton(val count: Int) : RoomListContentState
|
||||
data class Empty(val invitesState: InvitesState) : RoomListContentState
|
||||
data object Empty : RoomListContentState
|
||||
data class Rooms(
|
||||
val invitesState: InvitesState,
|
||||
val securityBannerState: SecurityBannerState,
|
||||
val summaries: ImmutableList<RoomListRoomSummary>,
|
||||
) : RoomListContentState
|
||||
|
||||
@@ -17,11 +17,15 @@
|
||||
package io.element.android.features.roomlist.impl
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomState
|
||||
import io.element.android.features.leaveroom.api.aLeaveRoomState
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.filters.aRoomListFiltersState
|
||||
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.features.roomlist.impl.model.aRoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.search.RoomListSearchState
|
||||
import io.element.android.features.roomlist.impl.search.aRoomListSearchState
|
||||
@@ -41,8 +45,6 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||
aRoomListState(),
|
||||
aRoomListState(snackbarMessage = SnackbarMessage(CommonStrings.common_verification_complete)),
|
||||
aRoomListState(hasNetworkConnection = false),
|
||||
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.SeenInvites)),
|
||||
aRoomListState(contentState = aRoomsContentState(invitesState = InvitesState.NewInvites)),
|
||||
aRoomListState(contextMenu = aContextMenuShown(roomName = "A nice room name")),
|
||||
aRoomListState(contextMenu = aContextMenuShown(isFavorite = true)),
|
||||
aRoomListState(contentState = aRoomsContentState(securityBannerState = SecurityBannerState.RecoveryKeyConfirmation)),
|
||||
@@ -64,6 +66,7 @@ internal fun aRoomListState(
|
||||
searchState: RoomListSearchState = aRoomListSearchState(),
|
||||
filtersState: RoomListFiltersState = aRoomListFiltersState(isFeatureEnabled = false),
|
||||
contentState: RoomListContentState = aRoomsContentState(),
|
||||
acceptDeclineInviteState: AcceptDeclineInviteState = anAcceptDeclineInviteState(),
|
||||
eventSink: (RoomListEvents) -> Unit = {}
|
||||
) = RoomListState(
|
||||
matrixUser = matrixUser,
|
||||
@@ -75,11 +78,23 @@ internal fun aRoomListState(
|
||||
filtersState = filtersState,
|
||||
searchState = searchState,
|
||||
contentState = contentState,
|
||||
acceptDeclineInviteState = acceptDeclineInviteState,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
||||
internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
return persistentListOf(
|
||||
aRoomListRoomSummary(
|
||||
name = "Room Invited",
|
||||
avatarData = AvatarData("!roomId", "Room with Alice and Bob", size = AvatarSize.RoomListItem),
|
||||
id = "!roomId:domain",
|
||||
inviteSender = InviteSender(
|
||||
userId = UserId("@bob:domain"),
|
||||
displayName = "Bob",
|
||||
avatarData = AvatarData("@bob:domain", "Bob", size = AvatarSize.InviteSender),
|
||||
),
|
||||
displayType = RoomSummaryDisplayType.INVITE,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = "Room",
|
||||
numberOfUnreadMessages = 1,
|
||||
@@ -98,11 +113,11 @@ internal fun aRoomListRoomSummaryList(): ImmutableList<RoomListRoomSummary> {
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
id = "!roomId3:domain",
|
||||
isPlaceholder = true,
|
||||
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
id = "!roomId4:domain",
|
||||
isPlaceholder = true,
|
||||
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -55,23 +55,17 @@ fun RoomListView(
|
||||
onSettingsClicked: () -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onRoomSettingsClicked: (roomId: RoomId) -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
onRoomDirectorySearchClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
acceptDeclineInviteView: @Composable () -> Unit,
|
||||
) {
|
||||
ConnectivityIndicatorContainer(
|
||||
modifier = modifier,
|
||||
isOnline = state.hasNetworkConnection,
|
||||
) { topPadding ->
|
||||
Box {
|
||||
fun onRoomLongClicked(
|
||||
roomListRoomSummary: RoomListRoomSummary
|
||||
) {
|
||||
state.eventSink(RoomListEvents.ShowContextMenu(roomListRoomSummary))
|
||||
}
|
||||
|
||||
if (state.contextMenu is RoomListState.ContextMenu.Shown) {
|
||||
RoomListContextMenu(
|
||||
contextMenu = state.contextMenu,
|
||||
@@ -83,21 +77,19 @@ fun RoomListView(
|
||||
LeaveRoomView(state = state.leaveRoomState)
|
||||
|
||||
RoomListScaffold(
|
||||
modifier = Modifier.padding(top = topPadding),
|
||||
state = state,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onOpenSettings = onSettingsClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
modifier = Modifier.padding(top = topPadding),
|
||||
)
|
||||
// This overlaid view will only be visible when state.displaySearchResults is true
|
||||
RoomListSearchView(
|
||||
state = state.searchState,
|
||||
eventSink = state.eventSink,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = { onRoomLongClicked(it) },
|
||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
@@ -105,6 +97,7 @@ fun RoomListView(
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
)
|
||||
acceptDeclineInviteView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,10 +108,8 @@ private fun RoomListScaffold(
|
||||
state: RoomListState,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onOpenSettings: () -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -153,9 +144,7 @@ private fun RoomListScaffold(
|
||||
eventSink = state.eventSink,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onRoomClicked = ::onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding)
|
||||
@@ -180,7 +169,7 @@ private fun RoomListScaffold(
|
||||
)
|
||||
}
|
||||
|
||||
internal fun RoomListRoomSummary.contentType() = isPlaceholder
|
||||
internal fun RoomListRoomSummary.contentType() = displayType.ordinal
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
@@ -191,9 +180,9 @@ internal fun RoomListViewPreview(@PreviewParameter(RoomListStateProvider::class)
|
||||
onSettingsClicked = {},
|
||||
onConfirmRecoveryKeyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {},
|
||||
onMenuActionClicked = {},
|
||||
onRoomDirectorySearchClicked = {},
|
||||
acceptDeclineInviteView = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -44,8 +44,6 @@ import androidx.compose.ui.unit.Velocity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomlist.impl.InvitesEntryPointView
|
||||
import io.element.android.features.roomlist.impl.InvitesState
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.features.roomlist.impl.RoomListContentState
|
||||
import io.element.android.features.roomlist.impl.RoomListContentStateProvider
|
||||
@@ -75,9 +73,7 @@ fun RoomListContentView(
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier = modifier) {
|
||||
@@ -92,8 +88,6 @@ fun RoomListContentView(
|
||||
}
|
||||
is RoomListContentState.Empty -> {
|
||||
EmptyView(
|
||||
state = contentState,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
)
|
||||
}
|
||||
@@ -104,8 +98,6 @@ fun RoomListContentView(
|
||||
eventSink = eventSink,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -128,30 +120,21 @@ private fun SkeletonView(count: Int, modifier: Modifier = Modifier) {
|
||||
|
||||
@Composable
|
||||
private fun EmptyView(
|
||||
state: RoomListContentState.Empty,
|
||||
onCreateRoomClicked: () -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
EmptyScaffold(
|
||||
title = R.string.screen_roomlist_empty_title,
|
||||
subtitle = R.string.screen_roomlist_empty_message,
|
||||
action = {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_start_chat),
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
||||
onClick = onCreateRoomClicked,
|
||||
)
|
||||
},
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
||||
}
|
||||
EmptyScaffold(
|
||||
title = R.string.screen_roomlist_empty_title,
|
||||
subtitle = R.string.screen_roomlist_empty_message,
|
||||
action = {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_start_chat),
|
||||
leadingIcon = IconSource.Vector(CompoundIcons.Compose()),
|
||||
onClick = onCreateRoomClicked,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -161,8 +144,6 @@ private fun RoomsView(
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.summaries.isEmpty() && filtersState.hasAnyFilterSelected) {
|
||||
@@ -176,8 +157,6 @@ private fun RoomsView(
|
||||
eventSink = eventSink,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -189,8 +168,6 @@ private fun RoomsViewList(
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onConfirmRecoveryKeyClicked: () -> Unit,
|
||||
onRoomClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onInvitesClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val lazyListState = rememberLazyListState()
|
||||
@@ -228,11 +205,6 @@ private fun RoomsViewList(
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
if (state.invitesState != InvitesState.NoInvites) {
|
||||
item {
|
||||
InvitesEntryPointView(onInvitesClicked, state.invitesState)
|
||||
}
|
||||
}
|
||||
// Note: do not use a key for the LazyColumn, or the scroll will not behave as expected if a room
|
||||
// is moved to the top of the list.
|
||||
itemsIndexed(
|
||||
@@ -242,7 +214,7 @@ private fun RoomsViewList(
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
if (index != state.summaries.lastIndex) {
|
||||
HorizontalDivider()
|
||||
@@ -305,8 +277,6 @@ internal fun RoomListContentViewPreview(@PreviewParameter(RoomListContentStatePr
|
||||
eventSink = {},
|
||||
onConfirmRecoveryKeyClicked = {},
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -20,15 +20,18 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Arrangement.Absolute.spacedBy
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -36,26 +39,35 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummaryProvider
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.designsystem.atomic.atoms.UnreadIndicatorAtom
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessage
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomMessageDate
|
||||
import io.element.android.libraries.designsystem.theme.roomListRoomName
|
||||
import io.element.android.libraries.designsystem.theme.unreadIndicator
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import timber.log.Timber
|
||||
|
||||
internal val minHeight = 84.dp
|
||||
|
||||
@@ -63,30 +75,67 @@ internal val minHeight = 84.dp
|
||||
internal fun RoomSummaryRow(
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (room.isPlaceholder) {
|
||||
RoomSummaryPlaceholderRow(
|
||||
modifier = modifier,
|
||||
)
|
||||
} else {
|
||||
RoomSummaryRealRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick,
|
||||
modifier = modifier
|
||||
)
|
||||
when (room.displayType) {
|
||||
RoomSummaryDisplayType.PLACEHOLDER -> {
|
||||
RoomSummaryPlaceholderRow(modifier = modifier)
|
||||
}
|
||||
RoomSummaryDisplayType.INVITE -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
Timber.d("Long click on invite room")
|
||||
},
|
||||
modifier = modifier
|
||||
) {
|
||||
InviteNameAndIndicatorRow(name = room.name)
|
||||
InviteSubtitle(isDirect = room.isDirect, inviteSender = room.inviteSender, canonicalAlias = room.canonicalAlias)
|
||||
if (!room.isDirect && room.inviteSender != null) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
InviteSenderRow(sender = room.inviteSender)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
InviteButtonsRow(
|
||||
onAcceptClicked = {
|
||||
eventSink(RoomListEvents.AcceptInvite(room))
|
||||
},
|
||||
onDeclineClicked = {
|
||||
eventSink(RoomListEvents.DeclineInvite(room))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
RoomSummaryDisplayType.ROOM -> {
|
||||
RoomSummaryScaffoldRow(
|
||||
room = room,
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
eventSink(RoomListEvents.ShowContextMenu(room))
|
||||
},
|
||||
modifier = modifier
|
||||
) {
|
||||
NameAndTimestampRow(
|
||||
name = room.name,
|
||||
timestamp = room.timestamp,
|
||||
isHighlighted = room.isHighlighted
|
||||
)
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun RoomSummaryRealRow(
|
||||
private fun RoomSummaryScaffoldRow(
|
||||
room: RoomListRoomSummary,
|
||||
onClick: (RoomListRoomSummary) -> Unit,
|
||||
onLongClick: (RoomListRoomSummary) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val clickModifier = Modifier.combinedClickable(
|
||||
onClick = { onClick(room) },
|
||||
@@ -100,94 +149,186 @@ private fun RoomSummaryRealRow(
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = minHeight)
|
||||
.then(clickModifier)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 11.dp)
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
Avatar(
|
||||
room
|
||||
.avatarData,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterVertically)
|
||||
)
|
||||
Avatar(room.avatarData)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp)
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NameAndTimestampRow(
|
||||
name: String,
|
||||
timestamp: String?,
|
||||
isHighlighted: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(16.dp)
|
||||
) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Timestamp
|
||||
Text(
|
||||
text = timestamp ?: "",
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = if (isHighlighted) {
|
||||
ElementTheme.colors.unreadIndicator
|
||||
} else {
|
||||
MaterialTheme.roomListRoomMessageDate()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteSubtitle(
|
||||
isDirect: Boolean,
|
||||
inviteSender: InviteSender?,
|
||||
canonicalAlias: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val subtitle = if (isDirect) {
|
||||
inviteSender?.userId?.value
|
||||
} else {
|
||||
canonicalAlias
|
||||
}
|
||||
if (subtitle != null) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LastMessageAndIndicatorRow(
|
||||
room: RoomListRoomSummary,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(28.dp)
|
||||
) {
|
||||
// Last Message
|
||||
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = attributedLastMessage,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Call and unread
|
||||
Row(
|
||||
modifier = Modifier.height(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
NameAndTimestampRow(room = room)
|
||||
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
|
||||
if (room.hasRoomCall) {
|
||||
OnGoingCallIcon(
|
||||
color = tint,
|
||||
)
|
||||
}
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
LastMessageAndIndicatorRow(room = room)
|
||||
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
||||
NotificationOffIndicatorAtom()
|
||||
} else if (room.numberOfUnreadMentions > 0) {
|
||||
MentionIndicatorAtom()
|
||||
}
|
||||
if (room.hasNewContent) {
|
||||
UnreadIndicatorAtom(
|
||||
color = tint
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.NameAndTimestampRow(room: RoomListRoomSummary) {
|
||||
// Name
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 16.dp),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = room.name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Timestamp
|
||||
Text(
|
||||
text = room.timestamp ?: "",
|
||||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = if (room.isHighlighted) {
|
||||
ElementTheme.colors.unreadIndicator
|
||||
} else {
|
||||
MaterialTheme.roomListRoomMessageDate()
|
||||
},
|
||||
)
|
||||
private fun InviteNameAndIndicatorRow(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = spacedBy(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
text = name,
|
||||
color = MaterialTheme.roomListRoomName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
UnreadIndicatorAtom(
|
||||
color = ElementTheme.colors.unreadIndicator
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.LastMessageAndIndicatorRow(room: RoomListRoomSummary) {
|
||||
// Last Message
|
||||
val attributedLastMessage = room.lastMessage as? AnnotatedString
|
||||
?: AnnotatedString(room.lastMessage.orEmpty().toString())
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(end = 28.dp),
|
||||
text = attributedLastMessage,
|
||||
color = MaterialTheme.roomListRoomMessage(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
minLines = 2,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
// Call and unread
|
||||
private fun InviteSenderRow(
|
||||
sender: InviteSender,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.height(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
val tint = if (room.isHighlighted) ElementTheme.colors.unreadIndicator else ElementTheme.colors.iconQuaternary
|
||||
if (room.hasRoomCall) {
|
||||
OnGoingCallIcon(
|
||||
color = tint,
|
||||
)
|
||||
}
|
||||
if (room.userDefinedNotificationMode == RoomNotificationMode.MUTE) {
|
||||
NotificationOffIndicatorAtom()
|
||||
} else if (room.numberOfUnreadMentions > 0) {
|
||||
MentionIndicatorAtom()
|
||||
}
|
||||
if (room.hasNewContent) {
|
||||
UnreadIndicatorAtom(
|
||||
color = tint
|
||||
)
|
||||
}
|
||||
Avatar(avatarData = sender.avatarData)
|
||||
Text(
|
||||
text = sender.annotatedString(),
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InviteButtonsRow(
|
||||
onAcceptClicked: () -> Unit,
|
||||
onDeclineClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.padding(),
|
||||
horizontalArrangement = spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = onDeclineClicked,
|
||||
size = ButtonSize.Medium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = onAcceptClicked,
|
||||
size = ButtonSize.Medium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +370,6 @@ internal fun RoomSummaryRowPreview(@PreviewParameter(RoomListRoomSummaryProvider
|
||||
RoomSummaryRow(
|
||||
room = data,
|
||||
onClick = {},
|
||||
onLongClick = {}
|
||||
eventSink = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.datasource
|
||||
|
||||
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.setValue
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.invite.api.SeenInvitesStore
|
||||
import io.element.android.features.roomlist.impl.InvitesState
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultInviteStateDataSource @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
private val seenInvitesStore: SeenInvitesStore,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
) : InviteStateDataSource {
|
||||
@Composable
|
||||
override fun inviteState(): InvitesState {
|
||||
val invites by client
|
||||
.roomListService
|
||||
.invites
|
||||
.summaries
|
||||
.collectAsState(initial = emptyList())
|
||||
|
||||
val seenInvites by seenInvitesStore
|
||||
.seenRoomIds()
|
||||
.collectAsState(initial = emptySet())
|
||||
|
||||
var state by remember { mutableStateOf(InvitesState.NoInvites) }
|
||||
|
||||
LaunchedEffect(invites, seenInvites) {
|
||||
withContext(coroutineDispatchers.computation) {
|
||||
state = when {
|
||||
invites.isEmpty() -> InvitesState.NoInvites
|
||||
seenInvites.containsAll(invites.roomIds) -> InvitesState.SeenInvites
|
||||
else -> InvitesState.NewInvites
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
||||
private val List<RoomSummary>.roomIds: Collection<RoomId>
|
||||
get() = filterIsInstance<RoomSummary.Filled>().map { it.details.roomId }
|
||||
@@ -16,13 +16,16 @@
|
||||
|
||||
package io.element.android.features.roomlist.impl.datasource
|
||||
|
||||
import io.element.android.features.roomlist.impl.model.InviteSender
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.libraries.core.extensions.orEmpty
|
||||
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -35,7 +38,7 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||
return RoomListRoomSummary(
|
||||
id = id,
|
||||
roomId = RoomId(id),
|
||||
isPlaceholder = true,
|
||||
displayType = RoomSummaryDisplayType.PLACEHOLDER,
|
||||
name = "Short name",
|
||||
timestamp = "hh:mm",
|
||||
lastMessage = "Last message for placeholder",
|
||||
@@ -46,8 +49,11 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||
isMarkedUnread = false,
|
||||
userDefinedNotificationMode = null,
|
||||
hasRoomCall = false,
|
||||
isDm = false,
|
||||
isDirect = false,
|
||||
isFavorite = false,
|
||||
inviteSender = null,
|
||||
isDm = false,
|
||||
canonicalAlias = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -73,11 +79,29 @@ class RoomListRoomSummaryFactory @Inject constructor(
|
||||
roomLastMessageFormatter.format(message.event, roomSummary.details.isDirect)
|
||||
}.orEmpty(),
|
||||
avatarData = avatarData,
|
||||
isPlaceholder = false,
|
||||
userDefinedNotificationMode = roomSummary.details.userDefinedNotificationMode,
|
||||
hasRoomCall = roomSummary.details.hasRoomCall,
|
||||
isDm = roomSummary.details.isDm,
|
||||
isDirect = roomSummary.details.isDirect,
|
||||
isFavorite = roomSummary.details.isFavorite,
|
||||
inviteSender = roomSummary.details.inviter?.run {
|
||||
InviteSender(
|
||||
userId = userId,
|
||||
displayName = displayName ?: "",
|
||||
avatarData = AvatarData(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = AvatarSize.InviteSender,
|
||||
),
|
||||
)
|
||||
},
|
||||
isDm = roomSummary.details.isDm,
|
||||
canonicalAlias = roomSummary.details.canonicalAlias,
|
||||
displayType = if (roomSummary.details.currentUserMembership == CurrentUserMembership.INVITED) {
|
||||
RoomSummaryDisplayType.INVITE
|
||||
} else {
|
||||
RoomSummaryDisplayType.ROOM
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,13 +26,15 @@ enum class RoomListFilter(val stringResource: Int) {
|
||||
Unread(R.string.screen_roomlist_filter_unreads),
|
||||
People(R.string.screen_roomlist_filter_people),
|
||||
Rooms(R.string.screen_roomlist_filter_rooms),
|
||||
Favourites(R.string.screen_roomlist_filter_favourites);
|
||||
Favourites(R.string.screen_roomlist_filter_favourites),
|
||||
Invites(R.string.screen_roomlist_filter_invites);
|
||||
|
||||
val oppositeFilter: RoomListFilter?
|
||||
val incompatibleFilters: Set<RoomListFilter>
|
||||
get() = when (this) {
|
||||
Rooms -> People
|
||||
People -> Rooms
|
||||
Unread -> null
|
||||
Favourites -> null
|
||||
Rooms -> setOf(People, Invites)
|
||||
People -> setOf(Rooms, Invites)
|
||||
Unread -> setOf(Invites)
|
||||
Favourites -> setOf(Invites)
|
||||
Invites -> setOf(Rooms, People, Unread, Favourites)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ data class RoomListFiltersEmptyStateResources(
|
||||
title = R.string.screen_roomlist_filter_favourites_empty_state_title,
|
||||
subtitle = R.string.screen_roomlist_filter_favourites_empty_state_subtitle
|
||||
)
|
||||
RoomListFilter.Invites -> RoomListFiltersEmptyStateResources(
|
||||
title = R.string.screen_roomlist_filter_invites_empty_state_title,
|
||||
subtitle = R.string.screen_roomlist_filter_mixed_empty_state_subtitle
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> RoomListFiltersEmptyStateResources(
|
||||
|
||||
@@ -66,6 +66,7 @@ class RoomListFiltersPresenter @Inject constructor(
|
||||
RoomListFilter.People -> MatrixRoomListFilter.Category.People
|
||||
RoomListFilter.Unread -> MatrixRoomListFilter.Unread
|
||||
RoomListFilter.Favourites -> MatrixRoomListFilter.Favorite
|
||||
RoomListFilter.Invites -> MatrixRoomListFilter.Invite
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -54,7 +54,7 @@ class DefaultFilterSelectionStrategy @Inject constructor() : FilterSelectionStra
|
||||
isSelected = true
|
||||
)
|
||||
}
|
||||
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.mapNotNull { it.oppositeFilter }.toSet()
|
||||
val unselectedFilters = RoomListFilter.entries - selectedFilters - selectedFilters.flatMap { it.incompatibleFilters }.toSet()
|
||||
val unselectedFilterStates = unselectedFilters.map {
|
||||
FilterSelectionState(
|
||||
filter = it,
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.model
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
@Immutable
|
||||
data class InviteSender(
|
||||
val userId: UserId,
|
||||
val displayName: String,
|
||||
val avatarData: AvatarData,
|
||||
) {
|
||||
@Composable
|
||||
fun annotatedString(): AnnotatedString {
|
||||
return stringResource(R.string.screen_invites_invited_you, displayName, userId.value).let { text ->
|
||||
val senderNameStart = LocalContext.current.getString(R.string.screen_invites_invited_you).indexOf("%1\$s")
|
||||
AnnotatedString(
|
||||
text = text,
|
||||
spanStyles = listOf(
|
||||
AnnotatedString.Range(
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
start = senderNameStart,
|
||||
end = senderNameStart + displayName.length
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,8 +24,10 @@ import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
@Immutable
|
||||
data class RoomListRoomSummary(
|
||||
val id: String,
|
||||
val displayType: RoomSummaryDisplayType,
|
||||
val roomId: RoomId,
|
||||
val name: String,
|
||||
val canonicalAlias: String?,
|
||||
val numberOfUnreadMessages: Int,
|
||||
val numberOfUnreadMentions: Int,
|
||||
val numberOfUnreadNotifications: Int,
|
||||
@@ -33,18 +35,21 @@ data class RoomListRoomSummary(
|
||||
val timestamp: String?,
|
||||
val lastMessage: CharSequence?,
|
||||
val avatarData: AvatarData,
|
||||
val isPlaceholder: Boolean,
|
||||
val userDefinedNotificationMode: RoomNotificationMode?,
|
||||
val hasRoomCall: Boolean,
|
||||
val isDirect: Boolean,
|
||||
val isDm: Boolean,
|
||||
val isFavorite: Boolean,
|
||||
) {
|
||||
val inviteSender: InviteSender?,
|
||||
) {
|
||||
val isHighlighted = userDefinedNotificationMode != RoomNotificationMode.MUTE &&
|
||||
(numberOfUnreadNotifications > 0 || numberOfUnreadMentions > 0) ||
|
||||
isMarkedUnread
|
||||
isMarkedUnread ||
|
||||
displayType == RoomSummaryDisplayType.INVITE
|
||||
|
||||
val hasNewContent = numberOfUnreadMessages > 0 ||
|
||||
numberOfUnreadMentions > 0 ||
|
||||
numberOfUnreadNotifications > 0 ||
|
||||
isMarkedUnread
|
||||
isMarkedUnread ||
|
||||
displayType == RoomSummaryDisplayType.INVITE
|
||||
}
|
||||
|
||||
@@ -20,13 +20,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
|
||||
open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSummary> {
|
||||
override val values: Sequence<RoomListRoomSummary>
|
||||
get() = sequenceOf(
|
||||
listOf(
|
||||
aRoomListRoomSummary(isPlaceholder = true),
|
||||
aRoomListRoomSummary(displayType = RoomSummaryDisplayType.PLACEHOLDER),
|
||||
aRoomListRoomSummary(),
|
||||
aRoomListRoomSummary(lastMessage = null),
|
||||
aRoomListRoomSummary(
|
||||
@@ -80,9 +81,37 @@ open class RoomListRoomSummaryProvider : PreviewParameterProvider<RoomListRoomSu
|
||||
)
|
||||
}.flatten()
|
||||
}.flatten(),
|
||||
listOf(
|
||||
aRoomListRoomSummary(
|
||||
displayType = RoomSummaryDisplayType.INVITE,
|
||||
inviteSender = anInviteSender(
|
||||
userId = "@alice:matrix.org",
|
||||
displayName = "Alice",
|
||||
),
|
||||
canonicalAlias = "#alias:matrix.org",
|
||||
),
|
||||
aRoomListRoomSummary(
|
||||
name = "Bob",
|
||||
displayType = RoomSummaryDisplayType.INVITE,
|
||||
inviteSender = anInviteSender(
|
||||
userId = "@bob:matrix.org",
|
||||
displayName = "Bob",
|
||||
),
|
||||
isDirect = true,
|
||||
)
|
||||
),
|
||||
).flatten()
|
||||
}
|
||||
|
||||
internal fun anInviteSender(
|
||||
userId: String,
|
||||
displayName: String,
|
||||
) = InviteSender(
|
||||
userId = UserId(userId),
|
||||
displayName = displayName,
|
||||
avatarData = AvatarData(userId, displayName, size = AvatarSize.InviteSender),
|
||||
)
|
||||
|
||||
internal fun aRoomListRoomSummary(
|
||||
id: String = "!roomId:domain",
|
||||
name: String = "Room name",
|
||||
@@ -92,12 +121,15 @@ internal fun aRoomListRoomSummary(
|
||||
isMarkedUnread: Boolean = false,
|
||||
lastMessage: String? = "Last message",
|
||||
timestamp: String? = lastMessage?.let { "88:88" },
|
||||
isPlaceholder: Boolean = false,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
hasRoomCall: Boolean = false,
|
||||
avatarData: AvatarData = AvatarData(id, name, size = AvatarSize.RoomListItem),
|
||||
isDirect: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
inviteSender: InviteSender? = null,
|
||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||
canonicalAlias: String? = null,
|
||||
) = RoomListRoomSummary(
|
||||
id = id,
|
||||
roomId = RoomId(id),
|
||||
@@ -109,9 +141,12 @@ internal fun aRoomListRoomSummary(
|
||||
timestamp = timestamp,
|
||||
lastMessage = lastMessage,
|
||||
avatarData = avatarData,
|
||||
isPlaceholder = isPlaceholder,
|
||||
userDefinedNotificationMode = notificationMode,
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDirect = isDirect,
|
||||
isDm = isDm,
|
||||
isFavorite = isFavorite,
|
||||
inviteSender = inviteSender,
|
||||
displayType = displayType,
|
||||
canonicalAlias = canonicalAlias,
|
||||
)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (c) 2023 New Vector Ltd
|
||||
* 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.
|
||||
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.roomlist.impl.datasource
|
||||
package io.element.android.features.roomlist.impl.model
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import io.element.android.features.roomlist.impl.InvitesState
|
||||
|
||||
interface InviteStateDataSource {
|
||||
@Composable
|
||||
fun inviteState(): InvitesState
|
||||
/**
|
||||
* Represents the type of display for a room list item.
|
||||
*/
|
||||
enum class RoomSummaryDisplayType {
|
||||
PLACEHOLDER,
|
||||
ROOM,
|
||||
INVITE
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.roomlist.impl.R
|
||||
import io.element.android.features.roomlist.impl.RoomListEvents
|
||||
import io.element.android.features.roomlist.impl.components.RoomSummaryRow
|
||||
import io.element.android.features.roomlist.impl.contentType
|
||||
import io.element.android.features.roomlist.impl.model.RoomListRoomSummary
|
||||
@@ -65,8 +66,8 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
@Composable
|
||||
internal fun RoomListSearchView(
|
||||
state: RoomListSearchState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomDirectorySearchClicked: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -90,7 +91,7 @@ internal fun RoomListSearchView(
|
||||
RoomListSearchContent(
|
||||
state = state,
|
||||
onRoomClicked = onRoomClicked,
|
||||
onRoomLongClicked = onRoomLongClicked,
|
||||
eventSink = eventSink,
|
||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||
)
|
||||
}
|
||||
@@ -102,8 +103,8 @@ internal fun RoomListSearchView(
|
||||
@Composable
|
||||
private fun RoomListSearchContent(
|
||||
state: RoomListSearchState,
|
||||
eventSink: (RoomListEvents) -> Unit,
|
||||
onRoomClicked: (RoomId) -> Unit,
|
||||
onRoomLongClicked: (RoomListRoomSummary) -> Unit,
|
||||
onRoomDirectorySearchClicked: () -> Unit,
|
||||
) {
|
||||
val borderColor = MaterialTheme.colorScheme.tertiary
|
||||
@@ -193,7 +194,7 @@ private fun RoomListSearchContent(
|
||||
RoomSummaryRow(
|
||||
room = room,
|
||||
onClick = ::onRoomClicked,
|
||||
onLongClick = onRoomLongClicked,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -220,7 +221,7 @@ internal fun RoomListSearchResultContentPreview(@PreviewParameter(RoomListSearch
|
||||
RoomListSearchContent(
|
||||
state = state,
|
||||
onRoomClicked = {},
|
||||
onRoomLongClicked = {},
|
||||
eventSink = {},
|
||||
onRoomDirectorySearchClicked = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="confirm_recovery_key_banner_message">"Your chat backup is currently out of sync. You need to enter your recovery key to maintain access to your chat backup."</string>
|
||||
<string name="confirm_recovery_key_banner_title">"Enter your recovery key"</string>
|
||||
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline the invitation to join %1$s?"</string>
|
||||
<string name="screen_invites_decline_chat_title">"Decline invite"</string>
|
||||
<string name="screen_invites_decline_direct_chat_message">"Are you sure you want to decline this private chat with %1$s?"</string>
|
||||
<string name="screen_invites_decline_direct_chat_title">"Decline chat"</string>
|
||||
<string name="screen_invites_empty_list">"No Invites"</string>
|
||||
<string name="screen_invites_invited_you">"%1$s (%2$s) invited you"</string>
|
||||
<string name="screen_migration_message">"This is a one time process, thanks for waiting."</string>
|
||||
<string name="screen_migration_title">"Setting up your account."</string>
|
||||
<string name="screen_roomlist_a11y_create_message">"Create a new conversation or room"</string>
|
||||
|
||||
@@ -21,14 +21,15 @@ import app.cash.molecule.moleculeFlow
|
||||
import app.cash.turbine.test
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import im.vector.app.features.analytics.plan.Interaction
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteEvents
|
||||
import io.element.android.features.invite.api.response.AcceptDeclineInviteState
|
||||
import io.element.android.features.invite.api.response.anAcceptDeclineInviteState
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomEvent
|
||||
import io.element.android.features.leaveroom.api.LeaveRoomPresenter
|
||||
import io.element.android.features.leaveroom.fake.FakeLeaveRoomPresenter
|
||||
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||
import io.element.android.features.networkmonitor.test.FakeNetworkMonitor
|
||||
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||
import io.element.android.features.roomlist.impl.datasource.FakeInviteDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.InviteStateDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersState
|
||||
@@ -52,6 +53,7 @@ import io.element.android.libraries.indicator.impl.DefaultIndicatorService
|
||||
import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomList
|
||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||
@@ -77,11 +79,14 @@ import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.MutablePresenter
|
||||
import io.element.android.tests.testutils.WarmUpRule
|
||||
import io.element.android.tests.testutils.consumeItemsUntilPredicate
|
||||
import io.element.android.tests.testutils.lambda.assert
|
||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
||||
import io.element.android.tests.testutils.lambda.value
|
||||
import io.element.android.tests.testutils.test
|
||||
import io.element.android.tests.testutils.testCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
@@ -303,38 +308,6 @@ class RoomListPresenterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - sets invite state`() = runTest {
|
||||
val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites)
|
||||
val inviteStateDataSource = FakeInviteDataSource(inviteStateFlow)
|
||||
val roomListService = FakeRoomListService()
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val presenter = createRoomListPresenter(
|
||||
inviteStateDataSource = inviteStateDataSource,
|
||||
coroutineScope = scope,
|
||||
client = FakeMatrixClient(roomListService = roomListService),
|
||||
)
|
||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
presenter.present()
|
||||
}.test {
|
||||
val firstItem = consumeItemsUntilPredicate {
|
||||
it.contentState is RoomListContentState.Rooms
|
||||
}.last()
|
||||
assertThat(firstItem.contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||
|
||||
inviteStateFlow.value = InvitesState.SeenInvites
|
||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.SeenInvites)
|
||||
|
||||
inviteStateFlow.value = InvitesState.NewInvites
|
||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NewInvites)
|
||||
|
||||
inviteStateFlow.value = InvitesState.NoInvites
|
||||
assertThat(awaitItem().contentAsRooms().invitesState).isEqualTo(InvitesState.NoInvites)
|
||||
scope.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - show context menu`() = runTest {
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
@@ -609,11 +582,53 @@ class RoomListPresenterTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - when a room is invited then accept and decline events are sent to acceptDeclinePresenter`() = runTest {
|
||||
val eventSinkRecorder = lambdaRecorder { _: AcceptDeclineInviteEvents -> }
|
||||
val acceptDeclinePresenter = Presenter {
|
||||
anAcceptDeclineInviteState(eventSink = eventSinkRecorder)
|
||||
}
|
||||
val roomListService = FakeRoomListService()
|
||||
val scope = CoroutineScope(coroutineContext + SupervisorJob())
|
||||
val matrixClient = FakeMatrixClient(
|
||||
roomListService = roomListService,
|
||||
)
|
||||
val roomSummary = aRoomSummaryFilled(
|
||||
currentUserMembership = CurrentUserMembership.INVITED
|
||||
)
|
||||
roomListService.postAllRoomsLoadingState(RoomList.LoadingState.Loaded(1))
|
||||
roomListService.postAllRooms(listOf(roomSummary))
|
||||
val presenter = createRoomListPresenter(
|
||||
coroutineScope = scope,
|
||||
client = matrixClient,
|
||||
acceptDeclineInvitePresenter = acceptDeclinePresenter
|
||||
)
|
||||
presenter.test {
|
||||
val state = consumeItemsUntilPredicate {
|
||||
it.contentState is RoomListContentState.Rooms
|
||||
}.last()
|
||||
|
||||
val roomListRoomSummary = state.contentAsRooms().summaries.first {
|
||||
it.id == roomSummary.identifier()
|
||||
}
|
||||
state.eventSink(RoomListEvents.AcceptInvite(roomListRoomSummary))
|
||||
state.eventSink(RoomListEvents.DeclineInvite(roomListRoomSummary))
|
||||
|
||||
val inviteData = roomListRoomSummary.toInviteData()
|
||||
|
||||
assert(eventSinkRecorder)
|
||||
.isCalledExactly(2)
|
||||
.withSequence(
|
||||
listOf(value(AcceptDeclineInviteEvents.AcceptInvite(inviteData))),
|
||||
listOf(value(AcceptDeclineInviteEvents.DeclineInvite(inviteData))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun TestScope.createRoomListPresenter(
|
||||
client: MatrixClient = FakeMatrixClient(),
|
||||
networkMonitor: NetworkMonitor = FakeNetworkMonitor(),
|
||||
snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(),
|
||||
inviteStateDataSource: InviteStateDataSource = FakeInviteDataSource(),
|
||||
leaveRoomPresenter: LeaveRoomPresenter = FakeLeaveRoomPresenter(),
|
||||
lastMessageTimestampFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter().apply {
|
||||
givenFormat(A_FORMATTED_DATE)
|
||||
@@ -626,11 +641,11 @@ class RoomListPresenterTests {
|
||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||
filtersPresenter: Presenter<RoomListFiltersState> = Presenter { aRoomListFiltersState() },
|
||||
searchPresenter: Presenter<RoomListSearchState> = Presenter { aRoomListSearchState() },
|
||||
acceptDeclineInvitePresenter: Presenter<AcceptDeclineInviteState> = Presenter { anAcceptDeclineInviteState() },
|
||||
) = RoomListPresenter(
|
||||
client = client,
|
||||
networkMonitor = networkMonitor,
|
||||
snackbarDispatcher = snackbarDispatcher,
|
||||
inviteStateDataSource = inviteStateDataSource,
|
||||
leaveRoomPresenter = leaveRoomPresenter,
|
||||
roomListDataSource = RoomListDataSource(
|
||||
roomListService = client.roomListService,
|
||||
@@ -652,5 +667,6 @@ class RoomListPresenterTests {
|
||||
sessionPreferencesStore = sessionPreferencesStore,
|
||||
filtersPresenter = filtersPresenter,
|
||||
analyticsService = analyticsService,
|
||||
acceptDeclineInvitePresenter = acceptDeclineInvitePresenter,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.roomlist.impl.components.RoomListMenuAction
|
||||
import io.element.android.features.roomlist.impl.model.RoomSummaryDisplayType
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
@@ -93,7 +94,9 @@ class RoomListViewTest {
|
||||
val state = aRoomListState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val room0 = state.contentAsRooms().summaries.first()
|
||||
val room0 = state.contentAsRooms().summaries.first {
|
||||
it.displayType == RoomSummaryDisplayType.ROOM
|
||||
}
|
||||
ensureCalledOnceWithParam(room0.roomId) { callback ->
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
@@ -109,7 +112,9 @@ class RoomListViewTest {
|
||||
val state = aRoomListState(
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
val room0 = state.contentAsRooms().summaries.first()
|
||||
val room0 = state.contentAsRooms().summaries.first {
|
||||
it.displayType == RoomSummaryDisplayType.ROOM
|
||||
}
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
)
|
||||
@@ -136,19 +141,20 @@ class RoomListViewTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking on invites invokes the expected callback`() {
|
||||
fun `clicking on accept and decline invite emits the expected Events`() {
|
||||
val eventsRecorder = EventsRecorder<RoomListEvents>()
|
||||
val state = aRoomListState(
|
||||
contentState = aRoomsContentState(invitesState = InvitesState.NewInvites),
|
||||
eventSink = eventsRecorder,
|
||||
)
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomListView(
|
||||
state = state,
|
||||
onInvitesClicked = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.action_invites_list)
|
||||
val invitedRoom = state.contentAsRooms().summaries.first {
|
||||
it.displayType == RoomSummaryDisplayType.INVITE
|
||||
}
|
||||
rule.setRoomListView(state = state)
|
||||
rule.clickOn(CommonStrings.action_accept)
|
||||
rule.clickOn(CommonStrings.action_decline)
|
||||
eventsRecorder.assertList(
|
||||
listOf(RoomListEvents.AcceptInvite(invitedRoom), RoomListEvents.DeclineInvite(invitedRoom)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +164,6 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||
onSettingsClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onConfirmRecoveryKeyClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onCreateRoomClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onInvitesClicked: () -> Unit = EnsureNeverCalled(),
|
||||
onRoomSettingsClicked: (RoomId) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onMenuActionClicked: (RoomListMenuAction) -> Unit = EnsureNeverCalledWithParam(),
|
||||
onRoomDirectorySearchClicked: () -> Unit = EnsureNeverCalled(),
|
||||
@@ -170,10 +175,10 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomL
|
||||
onSettingsClicked = onSettingsClicked,
|
||||
onConfirmRecoveryKeyClicked = onConfirmRecoveryKeyClicked,
|
||||
onCreateRoomClicked = onCreateRoomClicked,
|
||||
onInvitesClicked = onInvitesClicked,
|
||||
onRoomSettingsClicked = onRoomSettingsClicked,
|
||||
onMenuActionClicked = onMenuActionClicked,
|
||||
onRoomDirectorySearchClicked = onRoomDirectorySearchClicked,
|
||||
acceptDeclineInviteView = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.datasource
|
||||
|
||||
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.invite.test.FakeSeenInvitesStore
|
||||
import io.element.android.features.roomlist.impl.InvitesState
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID
|
||||
import io.element.android.libraries.matrix.test.A_ROOM_ID_2
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
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.runTest
|
||||
import org.junit.Test
|
||||
|
||||
internal class DefaultInviteStateDataSourceTest {
|
||||
@Test
|
||||
fun `emits NoInvites state if invites list is empty`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val seenStore = FakeSeenInvitesStore()
|
||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
dataSource.inviteState()
|
||||
}.test {
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits NewInvites state if unseen invite exists`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val seenStore = FakeSeenInvitesStore()
|
||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
dataSource.inviteState()
|
||||
}.test {
|
||||
skipItems(2)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits NewInvites state if multiple invites exist and at least one is unseen`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val seenStore = FakeSeenInvitesStore()
|
||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
dataSource.inviteState()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits SeenInvites state if invite exists in seen store`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val seenStore = FakeSeenInvitesStore()
|
||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true))
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
dataSource.inviteState()
|
||||
}.test {
|
||||
skipItems(1)
|
||||
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `emits new state in response to upstream events`() = runTest {
|
||||
val roomListService = FakeRoomListService()
|
||||
val client = FakeMatrixClient(roomListService = roomListService)
|
||||
val seenStore = FakeSeenInvitesStore()
|
||||
val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers())
|
||||
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
dataSource.inviteState()
|
||||
}.test {
|
||||
// Initially there are no invites
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||
|
||||
// When a single invite is received, state should be NewInvites
|
||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID)))
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||
|
||||
// If that invite is marked as seen, then the state becomes SeenInvites
|
||||
seenStore.publishRoomIds(setOf(A_ROOM_ID))
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.SeenInvites)
|
||||
|
||||
// Another new invite resets it to NewInvites
|
||||
roomListService.postInviteRooms(listOf(aRoomSummaryFilled(roomId = A_ROOM_ID), aRoomSummaryFilled(roomId = A_ROOM_ID_2)))
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NewInvites)
|
||||
|
||||
// All of the invites going away reverts to NoInvites
|
||||
roomListService.postInviteRooms(emptyList())
|
||||
skipItems(1)
|
||||
assertThat(awaitItem()).isEqualTo(InvitesState.NoInvites)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2023 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.datasource
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import io.element.android.features.roomlist.impl.InvitesState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
class FakeInviteDataSource(
|
||||
private val flow: Flow<InvitesState> = flowOf()
|
||||
) : InviteStateDataSource {
|
||||
@Composable
|
||||
override fun inviteState(): InvitesState {
|
||||
val state = flow.collectAsState(initial = InvitesState.NoInvites)
|
||||
return state.value
|
||||
}
|
||||
}
|
||||
@@ -64,6 +64,15 @@ class RoomListFiltersEmptyStateResourcesTest {
|
||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_favourites_empty_state_subtitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has only invites filter`() {
|
||||
val selectedFilters = listOf(RoomListFilter.Invites)
|
||||
val result = RoomListFiltersEmptyStateResources.fromSelectedFilters(selectedFilters)
|
||||
assertThat(result).isNotNull()
|
||||
assertThat(result?.title).isEqualTo(R.string.screen_roomlist_filter_invites_empty_state_title)
|
||||
assertThat(result?.subtitle).isEqualTo(R.string.screen_roomlist_filter_mixed_empty_state_subtitle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `fromSelectedFilters should return exact RoomListFiltersEmptyStateResources when selectedFilters has multiple filters`() {
|
||||
val selectedFilters = listOf(RoomListFilter.Unread, RoomListFilter.People, RoomListFilter.Rooms, RoomListFilter.Favourites)
|
||||
|
||||
@@ -45,6 +45,7 @@ class RoomListFiltersPresenterTests {
|
||||
filterSelectionState(RoomListFilter.People, false),
|
||||
filterSelectionState(RoomListFilter.Rooms, false),
|
||||
filterSelectionState(RoomListFilter.Favourites, false),
|
||||
filterSelectionState(RoomListFilter.Invites, false),
|
||||
)
|
||||
}
|
||||
cancelAndIgnoreRemainingEvents()
|
||||
@@ -84,6 +85,7 @@ class RoomListFiltersPresenterTests {
|
||||
filterSelectionState(RoomListFilter.People, false),
|
||||
filterSelectionState(RoomListFilter.Rooms, false),
|
||||
filterSelectionState(RoomListFilter.Favourites, false),
|
||||
filterSelectionState(RoomListFilter.Invites, false),
|
||||
).inOrder()
|
||||
assertThat(state.selectedFilters()).isEmpty()
|
||||
val roomListCurrentFilter = roomListService.allRooms.currentFilter.value as MatrixRoomListFilter.All
|
||||
|
||||
@@ -72,6 +72,15 @@ class RoomListRoomSummaryTest {
|
||||
assertThat(sut.isHighlighted).isTrue()
|
||||
assertThat(sut.hasNewContent).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when display type is invite then isHighlighted and hasNewContent are true`() {
|
||||
val sut = createRoomListRoomSummary(
|
||||
displayType = RoomSummaryDisplayType.INVITE,
|
||||
)
|
||||
assertThat(sut.isHighlighted).isTrue()
|
||||
assertThat(sut.hasNewContent).isTrue()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun createRoomListRoomSummary(
|
||||
@@ -81,6 +90,7 @@ internal fun createRoomListRoomSummary(
|
||||
isMarkedUnread: Boolean = false,
|
||||
userDefinedNotificationMode: RoomNotificationMode? = null,
|
||||
isFavorite: Boolean = false,
|
||||
displayType: RoomSummaryDisplayType = RoomSummaryDisplayType.ROOM,
|
||||
) = RoomListRoomSummary(
|
||||
id = A_ROOM_ID.value,
|
||||
roomId = A_ROOM_ID,
|
||||
@@ -92,9 +102,12 @@ internal fun createRoomListRoomSummary(
|
||||
timestamp = A_FORMATTED_DATE,
|
||||
lastMessage = "",
|
||||
avatarData = AvatarData(id = A_ROOM_ID.value, name = A_ROOM_NAME, size = AvatarSize.RoomListItem),
|
||||
isPlaceholder = false,
|
||||
displayType = displayType,
|
||||
userDefinedNotificationMode = userDefinedNotificationMode,
|
||||
hasRoomCall = false,
|
||||
isDm = false,
|
||||
isDirect = false,
|
||||
isFavorite = isFavorite,
|
||||
canonicalAlias = null,
|
||||
inviteSender = null,
|
||||
isDm = false,
|
||||
)
|
||||
|
||||
@@ -18,7 +18,3 @@ package io.element.android.libraries.deeplink
|
||||
|
||||
internal const val SCHEME = "elementx"
|
||||
internal const val HOST = "open"
|
||||
|
||||
object DeepLinkPaths {
|
||||
const val INVITE_LIST = "invites"
|
||||
}
|
||||
|
||||
@@ -36,13 +36,4 @@ class DeepLinkCreator @Inject constructor() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun inviteList(sessionId: SessionId): String {
|
||||
return buildString {
|
||||
append("$SCHEME://$HOST/")
|
||||
append(sessionId.value)
|
||||
append("/")
|
||||
append(DeepLinkPaths.INVITE_LIST)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,4 @@ sealed interface DeeplinkData {
|
||||
|
||||
/** The target is a room, with the given [sessionId], [roomId] and optionally a [threadId]. */
|
||||
data class Room(override val sessionId: SessionId, val roomId: RoomId, val threadId: ThreadId?) : DeeplinkData
|
||||
|
||||
/** The target is the invites list, with the given [sessionId]. */
|
||||
data class InviteList(override val sessionId: SessionId) : DeeplinkData
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ class DeeplinkParser @Inject constructor() {
|
||||
|
||||
return when (val screenPathComponent = pathBits.elementAtOrNull(1)) {
|
||||
null -> DeeplinkData.Root(sessionId)
|
||||
DeepLinkPaths.INVITE_LIST -> DeeplinkData.InviteList(sessionId)
|
||||
else -> {
|
||||
val roomId = screenPathComponent.let(::RoomId)
|
||||
val threadId = pathBits.elementAtOrNull(2)?.let(::ThreadId)
|
||||
|
||||
@@ -33,11 +33,4 @@ class DeepLinkCreatorTest {
|
||||
assertThat(sut.room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||
.isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun inviteList() {
|
||||
val sut = DeepLinkCreator()
|
||||
assertThat(sut.inviteList(A_SESSION_ID))
|
||||
.isEqualTo("elementx://open/@alice:server.org/invites")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,6 @@ class DeeplinkParserTest {
|
||||
"elementx://open/@alice:server.org/!aRoomId:domain"
|
||||
const val A_URI_WITH_ROOM_WITH_THREAD =
|
||||
"elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId"
|
||||
const val A_URI_FOR_INVITE_LIST =
|
||||
"elementx://open/@alice:server.org/invites"
|
||||
}
|
||||
|
||||
private val sut = DeeplinkParser()
|
||||
@@ -50,8 +48,6 @@ class DeeplinkParserTest {
|
||||
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, null))
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD)))
|
||||
.isEqualTo(DeeplinkData.Room(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID))
|
||||
assertThat(sut.getFromIntent(createIntent(A_URI_FOR_INVITE_LIST)))
|
||||
.isEqualTo(DeeplinkData.InviteList(A_SESSION_ID))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -57,6 +57,11 @@ sealed interface RoomListFilter {
|
||||
*/
|
||||
data object Favorite : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches rooms with Invited membership.
|
||||
*/
|
||||
data object Invite : RoomListFilter
|
||||
|
||||
/**
|
||||
* A filter that matches either Group or People rooms.
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.api.roomlist
|
||||
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
@@ -49,6 +50,7 @@ data class RoomSummaryDetails(
|
||||
val hasRoomCall: Boolean,
|
||||
val isDm: Boolean,
|
||||
val isFavorite: Boolean,
|
||||
val currentUserMembership: CurrentUserMembership,
|
||||
) {
|
||||
val lastMessageTimestamp = lastMessage?.originServerTs
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class RustMatrixClientFactory @Inject constructor(
|
||||
|
||||
val syncService = client.syncService()
|
||||
.withUtdHook(utdTracker)
|
||||
.withUnifiedInvitesInRoomList(true)
|
||||
.finish()
|
||||
|
||||
RustMatrixClient(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummary
|
||||
|
||||
@@ -25,21 +26,25 @@ val RoomListFilter.predicate
|
||||
is RoomListFilter.Any -> { _: RoomSummary -> true }
|
||||
RoomListFilter.None -> { _: RoomSummary -> false }
|
||||
RoomListFilter.Category.Group -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled && !roomSummary.details.isDirect
|
||||
roomSummary is RoomSummary.Filled && !roomSummary.details.isDirect && !roomSummary.isInvited()
|
||||
}
|
||||
RoomListFilter.Category.People -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.isDirect
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.isDirect && !roomSummary.isInvited()
|
||||
}
|
||||
RoomListFilter.Favorite -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.isFavorite
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.isFavorite && !roomSummary.isInvited()
|
||||
}
|
||||
RoomListFilter.Unread -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled &&
|
||||
!roomSummary.isInvited() &&
|
||||
(roomSummary.details.numUnreadNotifications > 0 || roomSummary.details.isMarkedUnread)
|
||||
}
|
||||
is RoomListFilter.NormalizedMatchRoomName -> { roomSummary: RoomSummary ->
|
||||
roomSummary is RoomSummary.Filled && roomSummary.details.name.contains(pattern, ignoreCase = true)
|
||||
}
|
||||
RoomListFilter.Invite -> { roomSummary: RoomSummary ->
|
||||
roomSummary.isInvited()
|
||||
}
|
||||
}
|
||||
|
||||
fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
|
||||
@@ -55,3 +60,5 @@ fun List<RoomSummary>.filter(filter: RoomListFilter): List<RoomSummary> {
|
||||
else -> filter(filter.predicate)
|
||||
}
|
||||
}
|
||||
|
||||
private fun RoomSummary.isInvited() = this is RoomSummary.Filled && this.details.currentUserMembership == CurrentUserMembership.INVITED
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.impl.roomlist
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.impl.notificationsettings.RoomNotificationSettingsMapper
|
||||
import io.element.android.libraries.matrix.impl.room.map
|
||||
import io.element.android.libraries.matrix.impl.room.member.RoomMemberMapper
|
||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
||||
import org.matrix.rustcomponents.sdk.RoomInfo
|
||||
@@ -45,6 +46,7 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
|
||||
hasRoomCall = roomInfo.hasRoomCall,
|
||||
isDm = roomInfo.isDirect && roomInfo.activeMembersCount.toLong() == 2L,
|
||||
isFavorite = roomInfo.isFavourite,
|
||||
currentUserMembership = roomInfo.membership.map(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package io.element.android.libraries.matrix.impl.roomlist
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.roomlist.RoomListFilter
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryDetails
|
||||
import io.element.android.libraries.matrix.test.room.aRoomSummaryFilled
|
||||
@@ -54,6 +55,11 @@ class RoomListFilterTests {
|
||||
name = "Room to search"
|
||||
)
|
||||
)
|
||||
private val invitedRoom = aRoomSummaryFilled(
|
||||
aRoomSummaryDetails(
|
||||
currentUserMembership = CurrentUserMembership.INVITED
|
||||
)
|
||||
)
|
||||
|
||||
private val roomSummaries = listOf(
|
||||
regularRoom,
|
||||
@@ -61,7 +67,8 @@ class RoomListFilterTests {
|
||||
favoriteRoom,
|
||||
markedAsUnreadRoom,
|
||||
unreadNotificationRoom,
|
||||
roomToSearch
|
||||
roomToSearch,
|
||||
invitedRoom
|
||||
)
|
||||
|
||||
@Test
|
||||
@@ -100,6 +107,12 @@ class RoomListFilterTests {
|
||||
assertThat(roomSummaries.filter(filter)).containsExactly(markedAsUnreadRoom, unreadNotificationRoom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Room list filter invites`() = runTest {
|
||||
val filter = RoomListFilter.Invite
|
||||
assertThat(roomSummaries.filter(filter)).containsExactly(invitedRoom)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Room list filter normalized match room name`() = runTest {
|
||||
val filter = RoomListFilter.NormalizedMatchRoomName("search")
|
||||
|
||||
@@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.test.room
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
@@ -40,6 +41,7 @@ fun aRoomSummaryFilled(
|
||||
numUnreadMentions: Int = 0,
|
||||
numUnreadMessages: Int = 0,
|
||||
notificationMode: RoomNotificationMode? = null,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = RoomSummary.Filled(
|
||||
aRoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
@@ -50,6 +52,7 @@ fun aRoomSummaryFilled(
|
||||
numUnreadMentions = numUnreadMentions,
|
||||
numUnreadMessages = numUnreadMessages,
|
||||
notificationMode = notificationMode,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -73,6 +76,7 @@ fun aRoomSummaryDetails(
|
||||
hasRoomCall: Boolean = false,
|
||||
isDm: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
@@ -89,6 +93,7 @@ fun aRoomSummaryDetails(
|
||||
hasRoomCall = hasRoomCall,
|
||||
isDm = isDm,
|
||||
isFavorite = isFavorite,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
|
||||
fun aRoomMessage(
|
||||
|
||||
@@ -44,6 +44,7 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.CurrentUserMembership
|
||||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.RoomNotificationMode
|
||||
import io.element.android.libraries.matrix.api.room.message.RoomMessage
|
||||
@@ -118,6 +119,7 @@ fun aRoomSummaryDetails(
|
||||
numUnreadNotifications: Int = 0,
|
||||
isMarkedUnread: Boolean = false,
|
||||
isFavorite: Boolean = false,
|
||||
currentUserMembership: CurrentUserMembership = CurrentUserMembership.JOINED,
|
||||
) = RoomSummaryDetails(
|
||||
roomId = roomId,
|
||||
name = name,
|
||||
@@ -134,4 +136,5 @@ fun aRoomSummaryDetails(
|
||||
numUnreadNotifications = numUnreadNotifications,
|
||||
isMarkedUnread = isMarkedUnread,
|
||||
isFavorite = isFavorite,
|
||||
currentUserMembership = currentUserMembership,
|
||||
)
|
||||
|
||||
@@ -30,9 +30,4 @@ interface IntentProvider {
|
||||
roomId: RoomId?,
|
||||
threadId: ThreadId?,
|
||||
): Intent
|
||||
|
||||
/**
|
||||
* Provide an intent to start the application on the invite list.
|
||||
*/
|
||||
fun getInviteListIntent(sessionId: SessionId): Intent
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ class NotificationCreator @Inject constructor(
|
||||
// .addAction(acceptInvitationActionFactory.create(inviteNotifiableEvent))
|
||||
.apply {
|
||||
// Build the pending intent for when the notification is clicked
|
||||
setContentIntent(pendingIntentFactory.createInviteListPendingIntent(inviteNotifiableEvent.sessionId))
|
||||
setContentIntent(pendingIntentFactory.createOpenRoomPendingIntent(inviteNotifiableEvent.sessionId, inviteNotifiableEvent.roomId))
|
||||
|
||||
if (inviteNotifiableEvent.noisy) {
|
||||
// Compat
|
||||
|
||||
@@ -19,7 +19,6 @@ package io.element.android.libraries.push.impl.notifications.factories
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import io.element.android.libraries.androidutils.uri.createIgnoredUri
|
||||
import io.element.android.libraries.di.ApplicationContext
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
@@ -128,9 +127,4 @@ class PendingIntentFactory @Inject constructor(
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
}
|
||||
|
||||
fun createInviteListPendingIntent(sessionId: SessionId): PendingIntent {
|
||||
val intent = intentProvider.getInviteListIntent(sessionId)
|
||||
return PendingIntentCompat.getActivity(context, 0, intent, 0, false)!!
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,4 @@ import io.element.android.libraries.push.impl.intent.IntentProvider
|
||||
|
||||
class FakeIntentProvider : IntentProvider {
|
||||
override fun getViewRoomIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?) = Intent()
|
||||
|
||||
override fun getInviteListIntent(sessionId: SessionId) = Intent()
|
||||
}
|
||||
|
||||
@@ -65,4 +65,5 @@ dependencies {
|
||||
implementation(projects.libraries.featureflag.impl)
|
||||
implementation(projects.services.analytics.noop)
|
||||
implementation(libs.coroutines.core)
|
||||
implementation(projects.libraries.push.test)
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import io.element.android.features.invite.impl.DefaultSeenInvitesStore
|
||||
import io.element.android.features.invite.impl.response.AcceptDeclineInvitePresenter
|
||||
import io.element.android.features.invite.impl.response.AcceptDeclineInviteView
|
||||
import io.element.android.features.leaveroom.impl.LeaveRoomPresenterImpl
|
||||
import io.element.android.features.networkmonitor.impl.NetworkMonitorImpl
|
||||
import io.element.android.features.roomlist.impl.RoomListPresenter
|
||||
import io.element.android.features.roomlist.impl.RoomListView
|
||||
import io.element.android.features.roomlist.impl.datasource.DefaultInviteStateDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListDataSource
|
||||
import io.element.android.features.roomlist.impl.datasource.RoomListRoomSummaryFactory
|
||||
import io.element.android.features.roomlist.impl.filters.RoomListFiltersPresenter
|
||||
@@ -50,6 +50,7 @@ import io.element.android.libraries.matrix.api.MatrixClient
|
||||
import io.element.android.libraries.matrix.api.core.RoomId
|
||||
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
|
||||
import io.element.android.libraries.preferences.impl.store.DefaultSessionPreferencesStore
|
||||
import io.element.android.libraries.push.test.notifications.FakeNotificationDrawerManager
|
||||
import io.element.android.services.analytics.noop.NoopAnalyticsService
|
||||
import io.element.android.services.toolbox.impl.strings.AndroidStringProvider
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -96,7 +97,6 @@ class RoomListScreen(
|
||||
client = matrixClient,
|
||||
networkMonitor = NetworkMonitorImpl(context, Singleton.appScope),
|
||||
snackbarDispatcher = SnackbarDispatcher(),
|
||||
inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers),
|
||||
leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers),
|
||||
roomListDataSource = RoomListDataSource(
|
||||
roomListService = matrixClient.roomListService,
|
||||
@@ -132,6 +132,11 @@ class RoomListScreen(
|
||||
featureFlagService = featureFlagService,
|
||||
filterSelectionStrategy = DefaultFilterSelectionStrategy(),
|
||||
),
|
||||
acceptDeclineInvitePresenter = AcceptDeclineInvitePresenter(
|
||||
client = matrixClient,
|
||||
analyticsService = NoopAnalyticsService(),
|
||||
notificationDrawerManager = FakeNotificationDrawerManager(),
|
||||
),
|
||||
analyticsService = NoopAnalyticsService(),
|
||||
)
|
||||
|
||||
@@ -154,11 +159,13 @@ class RoomListScreen(
|
||||
onSettingsClicked = {},
|
||||
onConfirmRecoveryKeyClicked = {},
|
||||
onCreateRoomClicked = {},
|
||||
onInvitesClicked = {},
|
||||
onRoomSettingsClicked = {},
|
||||
onMenuActionClicked = {},
|
||||
onRoomDirectorySearchClicked = {},
|
||||
modifier = modifier,
|
||||
acceptDeclineInviteView = {
|
||||
AcceptDeclineInviteView(state = state.acceptDeclineInviteState, onInviteAccepted = {}, onInviteDeclined = {})
|
||||
}
|
||||
)
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
|
||||
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user