Merge pull request #297 from csmith/102-show-invites-list

Feat: show invites list
This commit is contained in:
Chris Smith 2023-04-19 12:40:29 +01:00 committed by GitHub
commit 88360802af
75 changed files with 1376 additions and 79 deletions

View file

@ -31,6 +31,7 @@ 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
@ -39,6 +40,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.appnav.loggedin.LoggedInNode
import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.invitelist.api.InviteListEntryPoint
import io.element.android.features.preferences.api.PreferencesEntryPoint
import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
@ -68,6 +70,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val createRoomEntryPoint: CreateRoomEntryPoint,
private val appNavigationStateService: AppNavigationStateService,
private val verifySessionEntryPoint: VerifySessionEntryPoint,
private val inviteListEntryPoint: InviteListEntryPoint,
private val coroutineScope: CoroutineScope,
snackbarDispatcher: SnackbarDispatcher,
) : BackstackNode<LoggedInFlowNode.NavTarget>(
@ -142,6 +145,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
object VerifySession : NavTarget
@Parcelize
object InviteList: NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -166,6 +172,10 @@ class LoggedInFlowNode @AssistedInject constructor(
override fun onSessionVerificationClicked() {
backstack.push(NavTarget.VerifySession)
}
override fun onInvitesClicked() {
backstack.push(NavTarget.InviteList)
}
}
roomListEntryPoint
.nodeBuilder(this, buildContext)
@ -212,6 +222,17 @@ class LoggedInFlowNode @AssistedInject constructor(
NavTarget.VerifySession -> {
verifySessionEntryPoint.createNode(this, buildContext)
}
NavTarget.InviteList -> {
val callback = object : InviteListEntryPoint.Callback {
override fun onBackClicked() {
backstack.pop()
}
}
inviteListEntryPoint.nodeBuilder(this, buildContext)
.callback(callback)
.build()
}
}
}

View file

@ -22,6 +22,7 @@
* [Code coverage](#code-coverage)
* [Other points](#other-points)
* [Logging](#logging)
* [Translations](#translations)
* [Rageshake](#rageshake)
* [Tips](#tips)
* [Happy coding!](#happy-coding)
@ -392,6 +393,12 @@ Also generally it is recommended to provide the `Throwable` to the Timber log fu
Last point, note that `Timber.v` function may have no effect on some devices. Prefer using `Timber.d` and up.
#### Translations
Translations are handled through localazy. See [the dedicated README.md file](../tools/localazy/README.md) for information on how
to configure new modules etc.
#### Rageshake
Rageshake is a feature to send bug report directly from the application. Just shake your phone and you will be prompted to send a bug report.

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
plugins {
id("io.element.android-library")
}
android {
namespace = "io.element.android.features.invitelist.api"
}
dependencies {
implementation(projects.libraries.architecture)
}

View file

@ -0,0 +1,37 @@
/*
* 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.invitelist.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
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()
}
}

View file

@ -0,0 +1,53 @@
/*
* 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.
*/
// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
id("io.element.android-compose-library")
alias(libs.plugins.anvil)
alias(libs.plugins.ksp)
id("kotlin-parcelize")
}
android {
namespace = "io.element.android.features.invitelist.impl"
}
anvil {
generateDaggerFactories.set(true)
}
dependencies {
implementation(projects.anvilannotations)
anvil(projects.anvilcodegen)
api(projects.features.invitelist.api)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
implementation(projects.libraries.uiStrings)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)
testImplementation(libs.molecule.runtime)
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.libraries.matrix.test)
ksp(libs.showkase.processor)
}

View file

@ -0,0 +1,46 @@
/*
* 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.invitelist.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.invitelist.api.InviteListEntryPoint
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)
}
}
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.invitelist.impl
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.invitelist.api.InviteListEntryPoint
import io.element.android.libraries.di.SessionScope
@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() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
InviteListView(
state = state,
onBackClicked = ::onBackClicked,
)
}
}

View file

@ -0,0 +1,90 @@
/*
* 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.invitelist.impl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.room.RoomSummary
import kotlinx.collections.immutable.toPersistentList
import javax.inject.Inject
class InviteListPresenter @Inject constructor(
private val client: MatrixClient,
) : Presenter<InviteListState> {
@Composable
override fun present(): InviteListState {
val invites by client
.invitesDataSource
.roomSummaries()
.collectAsState()
return InviteListState(
inviteList = invites.mapNotNull(::toInviteSummary).toPersistentList(),
)
}
private fun toInviteSummary(roomSummary: RoomSummary): InviteListInviteSummary? {
return when (roomSummary) {
is RoomSummary.Filled -> roomSummary.details.run {
val i = inviter
val avatarData = if (isDirect && i != null)
AvatarData(
id = i.userId.value,
name = i.displayName,
url = i.avatarUrl,
)
else
AvatarData(
id = roomId.value,
name = name,
url = avatarURLString
)
val alias = if (isDirect)
inviter?.userId?.value
else
canonicalAlias
InviteListInviteSummary(
roomId = roomId,
roomName = name,
roomAlias = alias,
roomAvatarData = avatarData,
sender = if (isDirect) null else inviter?.let {
InviteSender(
userId = it.userId,
displayName = it.displayName ?: "",
avatarData = AvatarData(
id = it.userId.value,
name = it.displayName,
url = it.avatarUrl,
),
)
}
)
}
else -> null
}
}
}

View file

@ -0,0 +1,26 @@
/*
* 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.invitelist.impl
import androidx.compose.runtime.Immutable
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import kotlinx.collections.immutable.ImmutableList
@Immutable
data class InviteListState(
val inviteList: ImmutableList<InviteListInviteSummary>
)

View file

@ -0,0 +1,64 @@
/*
* 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.invitelist.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.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> {
override val values: Sequence<InviteListState>
get() = sequenceOf(
aInviteListState(),
aInviteListState().copy(inviteList = persistentListOf())
)
}
internal fun aInviteListState() = InviteListState(
inviteList = aInviteListInviteSummaryList(),
)
internal fun aInviteListInviteSummaryList(): ImmutableList<InviteListInviteSummary> {
return persistentListOf(
InviteListInviteSummary(
roomId = RoomId("!id1"),
roomName = "Room 1",
roomAlias = "#room:example.org",
sender = InviteSender(
userId = UserId("@alice:example.org"),
displayName = "Alice"
),
),
InviteListInviteSummary(
roomId = RoomId("!id2"),
roomName = "Room 2",
sender = InviteSender(
userId = UserId("@bob:example.org"),
displayName = "Bob"
),
),
InviteListInviteSummary(
roomId = RoomId("!id3"),
roomName = "Alice",
roomAlias = "@alice:example.com"
),
)
}

View file

@ -0,0 +1,129 @@
/*
* 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.invitelist.impl
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.items
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.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.features.invitelist.impl.components.InviteSummaryRow
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
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.R as StringR
@Composable
fun InviteListView(
state: InviteListState,
modifier: Modifier = Modifier,
onBackClicked: () -> Unit = {},
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
InviteListContent(
state = state,
modifier = modifier,
onBackClicked = onBackClicked,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InviteListContent(
state: InviteListState,
modifier: Modifier = Modifier,
onBackClicked: () -> Unit = {},
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Scaffold(
modifier = modifier,
topBar = {
TopAppBar(
navigationIcon = {
BackButton(onClick = onBackClicked)
},
title = {
Text(text = stringResource(StringR.string.action_invites_list))
}
)
},
content = { padding ->
Column(
modifier = Modifier.padding(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)
) {
items(
items = state.inviteList,
) { invite ->
InviteSummaryRow(
invite = invite,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
}
}
}
}
)
}
@Preview
@Composable
internal fun RoomListViewLightPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
ElementPreviewLight { ContentToPreview(state) }
@Preview
@Composable
internal fun RoomListViewDarkPreview(@PreviewParameter(InviteListStateProvider::class) state: InviteListState) =
ElementPreviewDark { ContentToPreview(state) }
@Composable
private fun ContentToPreview(state: InviteListState) {
InviteListView(state)
}

View file

@ -0,0 +1,211 @@
/*
* 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.invitelist.impl.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
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.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.Placeholder
import androidx.compose.ui.text.PlaceholderVerticalAlign
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.element.android.features.invitelist.impl.R
import io.element.android.features.invitelist.impl.model.InviteListInviteSummary
import io.element.android.features.invitelist.impl.model.InviteListInviteSummaryProvider
import io.element.android.features.invitelist.impl.model.InviteSender
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.RoomId
import kotlinx.collections.immutable.persistentMapOf
import io.element.android.libraries.ui.strings.R as StringR
private val minHeight = 72.dp
@Composable
internal fun InviteSummaryRow(
invite: InviteListInviteSummary,
modifier: Modifier = Modifier,
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Box(
modifier = modifier
.fillMaxWidth()
.heightIn(min = minHeight)
) {
DefaultInviteSummaryRow(
invite = invite,
onAcceptClicked = onAcceptClicked,
onDeclineClicked = onDeclineClicked,
)
}
}
@Composable
internal fun DefaultInviteSummaryRow(
invite: InviteListInviteSummary,
onAcceptClicked: (RoomId) -> Unit = {},
onDeclineClicked: (RoomId) -> Unit = {},
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp)
.height(IntrinsicSize.Min),
verticalAlignment = Alignment.Top
) {
Avatar(
invite.roomAvatarData,
)
Column(
modifier = Modifier
.padding(start = 12.dp, end = 4.dp)
.alignByBaseline()
.weight(1f)
) {
// Name
Text(
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
text = invite.roomName,
color = MaterialTheme.colorScheme.primary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// ID or Alias
invite.roomAlias?.let {
Text(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
text = it,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
// Sender
invite.sender?.let { sender ->
SenderRow(sender = sender)
}
// CTAs
Row(Modifier.padding(top = 12.dp)) {
OutlinedButton(
content = { Text(stringResource(StringR.string.action_decline), style = ElementTextStyles.Button) },
onClick = { onDeclineClicked(invite.roomId) },
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
)
Spacer(modifier = Modifier.width(12.dp))
Button(
content = { Text(stringResource(StringR.string.action_accept), style = ElementTextStyles.Button) },
onClick = { onAcceptClicked(invite.roomId) },
modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 7.dp),
)
}
}
}
}
@Composable
private fun SenderRow(sender: InviteSender) {
Text(
text = buildAnnotatedString {
val placeholder = "$"
val text = stringResource(R.string.screen_invites_invited_you, placeholder)
val nameIndex = text.indexOf(placeholder)
// Text before the placeholder
append(text.take(nameIndex))
// Avatar and display name
appendInlineContent("avatar")
withStyle(SpanStyle(fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.primary)) {
append(sender.displayName)
}
// Text after the placeholder
append(text.drop(nameIndex + placeholder.length))
},
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier.padding(top = 6.dp),
inlineContent = persistentMapOf(
"avatar" to InlineTextContent(
with(LocalDensity.current) {
Placeholder(20.dp.toSp(), 20.dp.toSp(), PlaceholderVerticalAlign.Center)
}
) {
Box(Modifier.fillMaxHeight().padding(end = 4.dp)) {
Avatar(
avatarData = sender.avatarData.copy(size = AvatarSize.Custom(16.dp)),
modifier = Modifier.align(Alignment.Center)
)
}
}
)
)
}
@Preview
@Composable
internal fun InviteSummaryRowLightPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) =
ElementPreviewLight { ContentToPreview(data) }
@Preview
@Composable
internal fun InviteSummaryRowDarkPreview(@PreviewParameter(InviteListInviteSummaryProvider::class) data: InviteListInviteSummary) =
ElementPreviewDark { ContentToPreview(data) }
@Composable
private fun ContentToPreview(data: InviteListInviteSummary) {
InviteSummaryRow(data)
}

View file

@ -0,0 +1,37 @@
/*
* 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.invitelist.impl.model
import androidx.compose.runtime.Immutable
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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),
val sender: InviteSender? = null
)
data class InviteSender(
val userId: UserId,
val displayName: String,
val avatarData: AvatarData = AvatarData(userId.value, displayName),
)

View file

@ -0,0 +1,39 @@
/*
* 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.invitelist.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:example.com"),
aInviteListInviteSummary().copy(roomName = "Alice", sender = null),
)
}
fun aInviteListInviteSummary() = InviteListInviteSummary(
roomId = RoomId("!room1"),
roomName = "Some room",
sender = InviteSender(
userId = UserId("@alice:example.org"),
displayName = "Alice"
),
)

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="screen_invites_decline_chat_message">"Are you sure you want to decline joining %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 to 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 invited you"</string>
</resources>

View file

@ -0,0 +1,184 @@
/*
* 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.invitelist.impl
import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.api.room.RoomSummary
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
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_NAME
import io.element.android.libraries.matrix.test.A_SESSION_ID
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.FakeRoomSummaryDataSource
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class InviteListPresenterTests {
@Test
fun `present - starts empty, adds invites when received`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
Truth.assertThat(initialState.inviteList).isEmpty()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_ROOM_NAME,
avatarURLString = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
)
)
)
)
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_ROOM_NAME)
}
}
@Test
fun `present - uses user ID and avatar for direct invites`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_USER_NAME,
avatarURLString = null,
isDirect = true,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].roomId).isEqualTo(A_ROOM_ID)
Truth.assertThat(withInviteState.inviteList[0].roomAlias).isEqualTo(A_USER_ID.value)
Truth.assertThat(withInviteState.inviteList[0].roomName).isEqualTo(A_USER_NAME)
Truth.assertThat(withInviteState.inviteList[0].roomAvatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
)
)
Truth.assertThat(withInviteState.inviteList[0].sender).isNull()
}
}
@Test
fun `present - includes sender details for room invites`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
invitesDataSource.postRoomSummary(
listOf(
RoomSummary.Filled(
RoomSummaryDetails(
roomId = A_ROOM_ID,
name = A_USER_NAME,
avatarURLString = null,
isDirect = false,
lastMessage = null,
lastMessageTimestamp = null,
unreadNotificationCount = 0,
inviter = RoomMember(
userId = A_USER_ID,
displayName = A_USER_NAME,
avatarUrl = AN_AVATAR_URL,
membership = RoomMembershipState.JOIN,
isNameAmbiguous = false,
powerLevel = 0,
normalizedPowerLevel = 0,
isIgnored = false,
)
)
)
)
)
val presenter = InviteListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource,
)
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val withInviteState = awaitItem()
Truth.assertThat(withInviteState.inviteList.size).isEqualTo(1)
Truth.assertThat(withInviteState.inviteList[0].sender?.displayName).isEqualTo(A_USER_NAME)
Truth.assertThat(withInviteState.inviteList[0].sender?.userId).isEqualTo(A_USER_ID)
Truth.assertThat(withInviteState.inviteList[0].sender?.avatarData).isEqualTo(
AvatarData(
id = A_USER_ID.value,
name = A_USER_NAME,
url = AN_AVATAR_URL,
)
)
}
}
}

View file

@ -34,8 +34,8 @@ interface RoomListEntryPoint : FeatureEntryPoint {
fun onRoomClicked(roomId: RoomId)
fun onCreateRoomClicked()
fun onSettingsClicked()
fun onSessionVerificationClicked()
fun onInvitesClicked()
}
}

View file

@ -52,6 +52,10 @@ class RoomListNode @AssistedInject constructor(
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
}
private fun onInvitesClicked() {
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
@ -62,6 +66,7 @@ class RoomListNode @AssistedInject constructor(
onOpenSettings = this::onOpenSettings,
onCreateRoomClicked = this::onCreateRoomClicked,
onVerifyClicked = this::onSessionVerificationClicked,
onInvitesClicked = this::onInvitesClicked,
)
}
}

View file

@ -85,6 +85,13 @@ class RoomListPresenter @Inject constructor(
initialLoad(matrixUser)
}
val invites by client
.invitesDataSource
.roomSummaries()
.collectAsState()
Timber.v("Invites size = ${invites.size}")
// Session verification status (unknown, not verified, verified)
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
@ -114,6 +121,7 @@ class RoomListPresenter @Inject constructor(
displayVerificationPrompt = displayVerificationPrompt,
snackbarMessage = snackbarMessage,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
displayInvites = invites.isNotEmpty(),
eventSink = ::handleEvents
)
}

View file

@ -30,5 +30,6 @@ data class RoomListState(
val displayVerificationPrompt: Boolean,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val displayInvites: Boolean,
val eventSink: (RoomListEvents) -> Unit
)

View file

@ -34,6 +34,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
aRoomListState().copy(displayVerificationPrompt = true),
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
aRoomListState().copy(hasNetworkConnection = false),
aRoomListState().copy(displayInvites = true),
)
}
@ -44,6 +45,7 @@ internal fun aRoomListState() = RoomListState(
hasNetworkConnection = true,
snackbarMessage = null,
displayVerificationPrompt = false,
displayInvites = false,
eventSink = {}
)

View file

@ -17,18 +17,23 @@
package io.element.android.features.roomlist.impl
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.ExperimentalMaterial3Api
@ -46,6 +51,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
@ -66,6 +72,8 @@ import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Scaffold
import io.element.android.libraries.designsystem.theme.components.Surface
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.roomListUnreadIndicator
import io.element.android.libraries.designsystem.utils.LogCompositions
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
@ -81,6 +89,7 @@ fun RoomListView(
onOpenSettings: () -> Unit = {},
onVerifyClicked: () -> Unit = {},
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
) {
RoomListContent(
state = state,
@ -89,6 +98,7 @@ fun RoomListView(
onOpenSettings = onOpenSettings,
onVerifyClicked = onVerifyClicked,
onCreateRoomClicked = onCreateRoomClicked,
onInvitesClicked = onInvitesClicked,
)
}
@ -101,6 +111,7 @@ fun RoomListContent(
onRoomClicked: (RoomId) -> Unit = {},
onOpenSettings: () -> Unit = {},
onCreateRoomClicked: () -> Unit = {},
onInvitesClicked: () -> Unit = {},
) {
fun onRoomClicked(room: RoomListRoomSummary) {
onRoomClicked(room.roomId)
@ -133,7 +144,7 @@ fun RoomListContent(
}
val snackbarHostState = remember { SnackbarHostState() }
val snackbarMessageText = if (state.snackbarMessage != null ) {
val snackbarMessageText = if (state.snackbarMessage != null) {
stringResource(state.snackbarMessage.messageResId)
} else null
val coroutineScope = rememberCoroutineScope()
@ -181,6 +192,27 @@ fun RoomListContent(
)
}
}
if (state.displayInvites) {
item {
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxSize()) {
TextButton(
content = {
Text(stringResource(StringR.string.action_invites_list))
Spacer(Modifier.size(8.dp))
Box(
modifier = Modifier
.size(12.dp)
.clip(CircleShape)
.background(MaterialTheme.roomListUnreadIndicator())
)
},
onClick = onInvitesClicked,
)
}
}
}
items(
items = state.roomList,
contentType = { room -> room.contentType() },

View file

@ -257,6 +257,35 @@ class RoomListPresenterTests {
}
}
@Test
fun `present - displays invites row if any invites exist`() = runTest {
val invitesDataSource = FakeRoomSummaryDataSource()
val presenter = RoomListPresenter(
FakeMatrixClient(
sessionId = A_SESSION_ID,
invitesDataSource = invitesDataSource
),
createDateFormatter(),
FakeRoomLastMessageFormatter(),
FakeSessionVerificationService(),
FakeNetworkMonitor(),
SnackbarDispatcher(),
)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(1)
Truth.assertThat(awaitItem().displayInvites).isFalse()
invitesDataSource.postRoomSummary(listOf(aRoomSummaryFilled()))
Truth.assertThat(awaitItem().displayInvites).isTrue()
invitesDataSource.postRoomSummary(listOf())
Truth.assertThat(awaitItem().displayInvites).isFalse()
}
}
private fun createDateFormatter(): LastMessageTimestampFormatter {
return FakeLastMessageTimestampFormatter().apply {
givenFormat(A_FORMATTED_DATE)

View file

@ -0,0 +1,94 @@
/*
* 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.libraries.designsystem.theme.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ButtonElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@Composable
fun OutlinedButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = ElementOutlinedButtonDefaults.shape,
colors: ButtonColors = ElementOutlinedButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ElementOutlinedButtonDefaults.buttonElevation(),
border: BorderStroke? = ElementOutlinedButtonDefaults.border,
contentPadding: PaddingValues = ElementOutlinedButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
androidx.compose.material3.Button(
onClick = onClick,
modifier = modifier,
enabled = enabled,
shape = shape,
colors = colors,
elevation = elevation,
border = border,
contentPadding = contentPadding,
interactionSource = interactionSource,
content = content,
)
}
object ElementOutlinedButtonDefaults {
val ContentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
val shape: Shape @Composable get() = ButtonDefaults.outlinedShape
val border: BorderStroke @Composable get() = ButtonDefaults.outlinedButtonBorder
@Composable
fun buttonElevation(): ButtonElevation = ButtonDefaults.buttonElevation()
@Composable
fun buttonColors(): ButtonColors = ButtonDefaults.outlinedButtonColors()
}
@Preview
@Composable
internal fun OutlinedButtonsLightPreview() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun OutlinedButtonsDarkPreview() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
OutlinedButton(onClick = {}, enabled = true) {
Text(text = "Click me! - Enabled")
}
OutlinedButton(onClick = {}, enabled = false) {
Text(text = "Click me! - Disabled")
}
}
}

View file

@ -32,6 +32,7 @@ import java.io.Closeable
interface MatrixClient : Closeable {
val sessionId: SessionId
val roomSummaryDataSource: RoomSummaryDataSource
val invitesDataSource: RoomSummaryDataSource
fun getRoom(roomId: RoomId): MatrixRoom?
fun findDM(userId: UserId): MatrixRoom?
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>

View file

@ -34,9 +34,11 @@ sealed interface RoomSummary {
data class RoomSummaryDetails(
val roomId: RoomId,
val name: String,
val canonicalAlias: String? = null,
val isDirect: Boolean,
val avatarURLString: String?,
val lastMessage: RoomMessage?,
val lastMessageTimestamp: Long?,
val unreadNotificationCount: Int,
val inviter: RoomMember? = null,
)

View file

@ -87,20 +87,18 @@ class RustMatrixClient constructor(
}
}
private val slidingSyncFilters by lazy {
SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
}
private val visibleRoomsSlidingSyncFilters = SlidingSyncRequestListFilters(
isDm = null,
spaces = emptyList(),
isEncrypted = null,
isInvite = false,
isTombstoned = false,
roomTypes = emptyList(),
notRoomTypes = listOf("m.space"),
roomNameLike = null,
tags = emptyList(),
notTags = emptyList()
)
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
.timelineLimit(limit = 1u)
@ -111,7 +109,7 @@ class RustMatrixClient constructor(
RequiredState(key = "m.room.join_rules", value = ""),
)
)
.filters(slidingSyncFilters)
.filters(visibleRoomsSlidingSyncFilters)
.name(name = "CurrentlyVisibleRooms")
.syncMode(mode = SlidingSyncMode.SELECTIVE)
.addRange(0u, 20u)
@ -119,12 +117,32 @@ class RustMatrixClient constructor(
it.build()
}
private val invitesSlidingSyncFilters = visibleRoomsSlidingSyncFilters.copy(isInvite = true)
private val invitesSlidingSyncList = SlidingSyncListBuilder()
.timelineLimit(limit = 1u)
.requiredState(
requiredState = listOf(
RequiredState(key = "m.room.avatar", value = ""),
RequiredState(key = "m.room.encryption", value = ""),
RequiredState(key = "m.room.canonical_alias", value = ""),
)
)
.filters(invitesSlidingSyncFilters)
.name(name = "CurrentInvites")
.syncMode(mode = SlidingSyncMode.SELECTIVE)
.addRange(0u, 20u)
.use {
it.build()
}
private val slidingSync = client
.slidingSync()
.homeserver("https://slidingsync.lab.matrix.org")
.withCommonExtensions()
.storageKey("ElementX")
.addList(visibleRoomsSlidingSyncList)
.addList(invitesSlidingSyncList)
.use {
it.build()
}
@ -142,6 +160,18 @@ class RustMatrixClient constructor(
override val roomSummaryDataSource: RoomSummaryDataSource
get() = rustRoomSummaryDataSource
private val rustInvitesDataSource: RustRoomSummaryDataSource =
RustRoomSummaryDataSource(
slidingSyncObserverProxy.updateSummaryFlow,
slidingSync,
invitesSlidingSyncList,
dispatchers,
::onRestartSync
)
override val invitesDataSource: RoomSummaryDataSource
get() = rustInvitesDataSource
private var slidingSyncObserverToken: TaskHandle? = null
private val mediaResolver = RustMediaResolver(this)
@ -152,6 +182,7 @@ class RustMatrixClient constructor(
init {
client.setDelegate(clientDelegate)
rustRoomSummaryDataSource.init()
rustInvitesDataSource.init()
slidingSync.setObserver(slidingSyncObserverProxy)
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow
.onEach { onSlidingSyncUpdate() }
@ -248,6 +279,7 @@ class RustMatrixClient constructor(
stopSync()
slidingSync.setObserver(null)
rustRoomSummaryDataSource.close()
rustInvitesDataSource.close()
client.setDelegate(null)
visibleRoomsSlidingSyncList.destroy()
slidingSync.destroy()

View file

@ -16,7 +16,6 @@
package io.element.android.libraries.matrix.impl.room
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.matrix.api.core.RoomId
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
@ -29,14 +28,17 @@ class RoomSummaryDetailsFactory(private val roomMessageFactory: RoomMessageFacto
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use {
roomMessageFactory.create(it)
}
return RoomSummaryDetails(
roomId = RoomId(slidingSyncRoom.roomId()),
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
canonicalAlias = room?.canonicalAlias(),
isDirect = slidingSyncRoom.isDm() ?: false,
avatarURLString = room?.avatarUrl(),
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
lastMessage = latestRoomMessage,
lastMessageTimestamp = latestRoomMessage?.originServerTs
lastMessageTimestamp = latestRoomMessage?.originServerTs,
inviter = room?.inviter()?.let(RoomMemberMapper::map),
)
}

View file

@ -41,6 +41,7 @@ class FakeMatrixClient(
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
private val pushersService: FakePushersService = FakePushersService(),
private val notificationService: FakeNotificationService = FakeNotificationService(),

View file

@ -54,6 +54,7 @@
<string name="action_view_source">"View Source"</string>
<string name="action_yes">"Yes"</string>
<string name="common_about">"About"</string>
<string name="common_analytics">"Analytics"</string>
<string name="common_audio">"Audio"</string>
<string name="common_bubbles">"Bubbles"</string>
<string name="common_creating_room">"Creating room…"</string>
@ -181,8 +182,17 @@
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_invites_empty_list">"No Invites"</string>
<string name="screen_invites_invited_you">"%1$s invited you"</string>
<string name="screen_analytics_help_us_improve">"Help us identify issues and improve %1$s by sharing anonymous usage data."</string>
<string name="screen_analytics_prompt_data_usage">"We "<b>"don\'t"</b>" record or profile any account data"</string>
<string name="screen_analytics_prompt_help_us_improve">"Help us identify issues and improve %1$s by sharing anonymous usage data."</string>
<string name="screen_analytics_prompt_read_terms">"You can read all our terms %1$s."</string>
<string name="screen_analytics_prompt_read_terms_content_link">"here"</string>
<string name="screen_analytics_prompt_settings">"You can turn this off anytime in settings"</string>
<string name="screen_analytics_prompt_third_party_sharing">"We "<b>"don\'t"</b>" share information with third parties"</string>
<string name="screen_analytics_prompt_title">"Help improve %1$s"</string>
<string name="screen_analytics_read_terms">"You can read all our terms %1$s."</string>
<string name="screen_analytics_read_terms_content_link">"here"</string>
<string name="screen_analytics_share_data">"Share analytics data"</string>
<string name="screen_report_content_block_user">"Block user"</string>
<string name="screen_report_content_block_user_hint">"Check if you want to hide all current and future messages from this user"</string>
<string name="screen_room_member_details_block_alert_action">"Block"</string>

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ae7f920795745910f0df3a472d31b055fed52a9041a0b7894f9a732be8777b2e
size 22344

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:52d02a23a34fbf2e3ba41547a10ecb774d8ef40940ef9cc30b79b98fbafe267e
size 25888

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e9622f36ef930ce3b81e656c0447d971b9c2939ae4b48268689bf1256a6c9249
size 16978

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5f94f5fac1646765a8b07712d046c7446c8c019384e727899178ef0bd8bd78fb
size 21545

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0272e8fc322038a054a94b516a0c36136da0c1d244790d72cfde97de85845d0
size 25178

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5cfeafe4170c00c0344ec5542279e947eb0a7cc4d6558a4c79564d6fc2fb3bab
size 16241

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4e085e53b63f83a51bb726fc3f38f37c8fb6412e5b8ed3ec893b24c87fa63a4
size 56723

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bb46d54ea5b341f34284c5478045fae971004ef2c4208f1a57743868e2058c80
size 9312

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:151331de0ea941330ff611e312833aa9c731ed0cd49ed4f31f724dbb5c7ba252
size 55174

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3f4ce908e84c489268907758d54a44711d84a05947d34177f6408b1a78f2f494
size 8850

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
size 6302
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
size 6303

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
size 7036
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
size 7035

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
size 6302
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
size 6303

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
size 7036
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
size 7035

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
size 6161
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
size 6163

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
size 6836
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
size 6834

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
size 6161
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
size 6163

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
size 6836
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
size 6834

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3dc781e3f17f09eb7e0cb8bf753a68ebde850b0b618e8d6bff7c780e365ee778
size 11853
oid sha256:ff1f9f65252e855037d6008aea0d884ccaec6419412b26e4ad12a32ab583f5ca
size 11848

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6d14b21a4723db7ac5ceda97eee44681d5d236007262e0e9e8b5a8d5abf8022b
oid sha256:37c370bc41527d7b407679c971a35aee051da93c033c2f195b6c3d6c7b2d885a
size 11514

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:562e83b7771ff2209c7882180af55a8f3123c12c020b294858b1b46bff8a8a95
size 30928
oid sha256:5315b975f8b9def0b0d7575160cb34300511f1b83845ee2d1762188481cf6fe9
size 30927

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79b16cbbbf36e6de4ea6270476371c7097424b26d4066f7156738c084fe061de
oid sha256:099401d03621aca301d3c4590d90ba4e4642d400d7603a8e15140a1bfef0c0ec
size 42758

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702
size 32721
oid sha256:ca5dbc3aa00d6116de38fc0217b20ab0db4096b1f76bcc18b43ab9fd343f8110
size 32722

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc
size 44568
oid sha256:6211ec07d10d48e4713206e4c1fe24eae890fe2310d5bcc1f4ca54cc57f84ec9
size 44564

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8
size 28911
oid sha256:8ac7985e70782a84fc351cb15536bfd700ca8d6d48d5da0ee9dcaf5c78226479
size 28909

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096
size 45979
oid sha256:c99138940ed1a8beac7da3c673600016a03b87195e07f205d5d389ef05be0fb0
size 45978

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:34469e4b86a441fb6f7cfbdce4b622109c093a234ae5d35d895dce35ef10c20c
size 30327
oid sha256:2f24112ad89e5ef93917d45e3232043168d5092ce6d62a7d414773dd0afda963
size 30326

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:11c677ab3124a94f531f1db14eb2b2f19a493470620f7375cf7c1d14056cdbc9
size 42429
oid sha256:9c696c464515873a94d28f8909dc47255717fc3b012ce0d80000769714e3cb9c
size 42427

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f
size 32077
oid sha256:0fe506e4e7adbfe19fd48f0a40c19343471e0bb06e12fac8d5e6d54595e7193a
size 32076

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff
size 44431
oid sha256:c9c9374c303c9a71b0526fccfd18903d145c1b3b0059de3ccd41cf80f1541a80
size 44427

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15
size 28320
oid sha256:5f18d8b33eace857f78b92eed035621125bbfca1f0d43e5e47fd1c3965324de0
size 28319

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c
size 45641
oid sha256:be0722a804c2fc264f931f8784635bceab7ddcf08e378bed8ea510bd76813fbf
size 45642

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d6de6a4dee8a62839c15a84de8cb9817b3de8ae9fd2317e723c47bb679a72b7d
size 39603
oid sha256:1298db69feb09f55f2c020a9e5262f8a462700d2bd481af1c2fa7f54069069f8
size 39602

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8b1012920df3d9e6754e43464723139ae3fd0c04a518f761afcd54e25105acd6
size 41437
oid sha256:4a6cf87a5664b751e96e9bc9741f0d7d78ce59b8805ef311d308fc6c2da694b7
size 41436

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:caaf172ddb39cbd1a77bfb2295202e5c0aa95c846353a17da6abe6c50316cf63
size 38510
oid sha256:75de58862f2801ce1d51e9f96fc68968a4c92e7caa9b71a1bcd7228905191990
size 38513

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59c0a62bf960d150ee111e88a932e9f5c85a82336a787b10db9f3c157ab13a35
size 40520
oid sha256:b553c8482b5c7d129fc83dee9e0f9ff6926c9b31f19e0786281b9a8e71706472
size 40522

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f
size 60019
oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93
size 60007

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316
size 57588
oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84
size 57580

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9630d9b5e3d26b677eaa8c0496d76d73f95d28a487ef0ef845d3a89747d6a8cb
size 179668
oid sha256:3c6371800946673d56bf06020f79a106f41474a4bc7664abbdf57c4228352611
size 179661

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2f8ad5f69dbcda0284f731a01851fc73dc93ba9b1fad9e778b23f0155bfc7107
size 178346
oid sha256:7feb6804ceca70ed0081ef36f23ba5623e263d05b0c12d5f08ba81c62f9ec492
size 178343

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613
size 39762

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ebb15a58e5d3497b2819f5a6b6fde88962f01cc2ae02d4b8cda0e45e1dde677f
size 39314

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6428b3de7e12b0eac27ab969605e35fed78dd3193998cd719387421330e91321
size 16396

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8e4e8e38986aa729d5de1384ea79620cdb9e01d001712c7a78d001ebe5bef623
size 15255

View file

@ -25,6 +25,12 @@
"screen_onboarding_.*"
]
},
{
"name": ":features:invitelist:impl",
"includeRegex": [
"screen_invites_.*"
]
},
{
"name": ":features:createroom:impl",
"includeRegex": [