Merge pull request #2714 from element-hq/feature/fga/room_list_invites
[Feature] Room list invites
This commit is contained in:
commit
0cda5b9e90
291 changed files with 842 additions and 2278 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:730f27b45cb2176ac6f8415780b74e2f117b9cc3f64d4602e6c42253f65b23e7
|
||||
size 29181
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:5bcafa377dfdd0a3e3db955c2d81791f1c36c95b22d978090b8d4e6b063c7d69
|
||||
size 34866
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0b77b77a20d1c3d936dea733542f1b2bd8e575ccb6d3021ce1c2d84a701bea35
|
||||
size 34965
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:d3092c176ec732f3e378430779a8645c02cd6c47756a6dafaf09a7fe28e79608
|
||||
size 14112
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:8b3cf23c4ef3ee18a3dd4a20a683266b2cdc6b6bca90b1006c109e23c82c6ebb
|
||||
size 29445
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:182d46896d7f5cd32b3234cf1201934b184cb80288cf3561f5f84c2dc69726db
|
||||
size 27951
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f55a0e24a5b0e1a15f017d9989409ca7e4dc636f2bf306c3accdca7efdc9fcc5
|
||||
size 32451
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:ef4d52dab3875b49c2160c91e0eae5c492cfde4dfb66b95a003e833e15373e4a
|
||||
size 32555
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a5c44be48cb324b9924284c56c5e78c2e118d5939a4a72fe9b6e161350beef96
|
||||
size 13179
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68fef9689570c6353ae8620c98e9f081de6dd0ccb34529a9580cac67e08d676f
|
||||
size 28109
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa90fb393dcb986721190468a83b7b9956e97e2a89d809913ea8895b0f698eba
|
||||
size 54691
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9e362f7b4f1b4dfe169a0adda60075ec1533fcdf0613b07267e8316746c802d2
|
||||
size 9013
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:aa90fb393dcb986721190468a83b7b9956e97e2a89d809913ea8895b0f698eba
|
||||
size 54691
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:25e6524ca89091a336fb85ba1943dea5c75aed47c991f5e88e610f7248a37d00
|
||||
size 53926
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb75ce275178fd84581680df4ba8cfe0cc4a5db079160a0a1c22e717521efb56
|
||||
size 54607
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dfe58becc60142b7547331dc4c263e761930dcf0dbb11d4e1efcbfaf618cb31
|
||||
size 42625
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dfe58becc60142b7547331dc4c263e761930dcf0dbb11d4e1efcbfaf618cb31
|
||||
size 42625
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb2e96e4737a2616676439c4d6ce85279c7ea9f09129058818502658841e8f10
|
||||
size 51858
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:434cec9370a229fd8f2a2f5ee7b7c0128f15a2fe1c011ef0c1689845d0536aaa
|
||||
size 8753
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:cb2e96e4737a2616676439c4d6ce85279c7ea9f09129058818502658841e8f10
|
||||
size 51858
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:589b25bccb76c33aa5d8dbd83abfc96040825b2d7ab6ae8f6b2e8de97b181172
|
||||
size 48907
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c5fd489d8088e8afd11738f52417cff00ff720ea493ac9b8d350b10c1b1effd0
|
||||
size 49458
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76f3386f41e0b2daad020c08e577e9f7266c8f80e2cb020c6f6d4f51126d182b
|
||||
size 37982
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:76f3386f41e0b2daad020c08e577e9f7266c8f80e2cb020c6f6d4f51126d182b
|
||||
size 37982
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue