Invite list view
Adds a CTA on the room list to view invites if there are any. The invite list presents each invite with accept/decline buttons and (for room invites) the sender details. Fixes #102
This commit is contained in:
parent
44a2597300
commit
e90211b5d8
75 changed files with 1376 additions and 79 deletions
|
|
@ -31,6 +31,7 @@ import com.bumble.appyx.core.node.node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
import com.bumble.appyx.core.plugin.plugins
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import com.bumble.appyx.navmodel.backstack.BackStack
|
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.push
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.replace
|
import com.bumble.appyx.navmodel.backstack.operation.replace
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.singleTop
|
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.anvilannotations.ContributesNode
|
||||||
import io.element.android.appnav.loggedin.LoggedInNode
|
import io.element.android.appnav.loggedin.LoggedInNode
|
||||||
import io.element.android.features.createroom.api.CreateRoomEntryPoint
|
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.preferences.api.PreferencesEntryPoint
|
||||||
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
import io.element.android.features.roomlist.api.RoomListEntryPoint
|
||||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||||
|
|
@ -68,6 +70,7 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
private val createRoomEntryPoint: CreateRoomEntryPoint,
|
||||||
private val appNavigationStateService: AppNavigationStateService,
|
private val appNavigationStateService: AppNavigationStateService,
|
||||||
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
private val verifySessionEntryPoint: VerifySessionEntryPoint,
|
||||||
|
private val inviteListEntryPoint: InviteListEntryPoint,
|
||||||
private val coroutineScope: CoroutineScope,
|
private val coroutineScope: CoroutineScope,
|
||||||
snackbarDispatcher: SnackbarDispatcher,
|
snackbarDispatcher: SnackbarDispatcher,
|
||||||
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
) : BackstackNode<LoggedInFlowNode.NavTarget>(
|
||||||
|
|
@ -142,6 +145,9 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object VerifySession : NavTarget
|
object VerifySession : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object InviteList: NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||||
|
|
@ -166,6 +172,10 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
override fun onSessionVerificationClicked() {
|
override fun onSessionVerificationClicked() {
|
||||||
backstack.push(NavTarget.VerifySession)
|
backstack.push(NavTarget.VerifySession)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onInvitesClicked() {
|
||||||
|
backstack.push(NavTarget.InviteList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
roomListEntryPoint
|
roomListEntryPoint
|
||||||
.nodeBuilder(this, buildContext)
|
.nodeBuilder(this, buildContext)
|
||||||
|
|
@ -212,6 +222,17 @@ class LoggedInFlowNode @AssistedInject constructor(
|
||||||
NavTarget.VerifySession -> {
|
NavTarget.VerifySession -> {
|
||||||
verifySessionEntryPoint.createNode(this, buildContext)
|
verifySessionEntryPoint.createNode(this, buildContext)
|
||||||
}
|
}
|
||||||
|
NavTarget.InviteList -> {
|
||||||
|
val callback = object : InviteListEntryPoint.Callback {
|
||||||
|
override fun onBackClicked() {
|
||||||
|
backstack.pop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inviteListEntryPoint.nodeBuilder(this, buildContext)
|
||||||
|
.callback(callback)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
* [Code coverage](#code-coverage)
|
* [Code coverage](#code-coverage)
|
||||||
* [Other points](#other-points)
|
* [Other points](#other-points)
|
||||||
* [Logging](#logging)
|
* [Logging](#logging)
|
||||||
|
* [Translations](#translations)
|
||||||
* [Rageshake](#rageshake)
|
* [Rageshake](#rageshake)
|
||||||
* [Tips](#tips)
|
* [Tips](#tips)
|
||||||
* [Happy coding!](#happy-coding)
|
* [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.
|
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
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
|
||||||
27
features/invitelist/api/build.gradle.kts
Normal file
27
features/invitelist/api/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
53
features/invitelist/impl/build.gradle.kts
Normal file
53
features/invitelist/impl/build.gradle.kts
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
)
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
|
)
|
||||||
|
|
@ -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"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -34,8 +34,8 @@ interface RoomListEntryPoint : FeatureEntryPoint {
|
||||||
fun onRoomClicked(roomId: RoomId)
|
fun onRoomClicked(roomId: RoomId)
|
||||||
fun onCreateRoomClicked()
|
fun onCreateRoomClicked()
|
||||||
fun onSettingsClicked()
|
fun onSettingsClicked()
|
||||||
|
|
||||||
fun onSessionVerificationClicked()
|
fun onSessionVerificationClicked()
|
||||||
|
fun onInvitesClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ class RoomListNode @AssistedInject constructor(
|
||||||
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onSessionVerificationClicked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onInvitesClicked() {
|
||||||
|
plugins<RoomListEntryPoint.Callback>().forEach { it.onInvitesClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
|
|
@ -62,6 +66,7 @@ class RoomListNode @AssistedInject constructor(
|
||||||
onOpenSettings = this::onOpenSettings,
|
onOpenSettings = this::onOpenSettings,
|
||||||
onCreateRoomClicked = this::onCreateRoomClicked,
|
onCreateRoomClicked = this::onCreateRoomClicked,
|
||||||
onVerifyClicked = this::onSessionVerificationClicked,
|
onVerifyClicked = this::onSessionVerificationClicked,
|
||||||
|
onInvitesClicked = this::onInvitesClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ class RoomListPresenter @Inject constructor(
|
||||||
initialLoad(matrixUser)
|
initialLoad(matrixUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val invites by client
|
||||||
|
.invitesDataSource
|
||||||
|
.roomSummaries()
|
||||||
|
.collectAsState()
|
||||||
|
|
||||||
|
Timber.v("Invites size = ${invites.size}")
|
||||||
|
|
||||||
// Session verification status (unknown, not verified, verified)
|
// Session verification status (unknown, not verified, verified)
|
||||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||||
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
var verificationPromptDismissed by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
@ -114,6 +121,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
displayVerificationPrompt = displayVerificationPrompt,
|
displayVerificationPrompt = displayVerificationPrompt,
|
||||||
snackbarMessage = snackbarMessage,
|
snackbarMessage = snackbarMessage,
|
||||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||||
|
displayInvites = invites.isNotEmpty(),
|
||||||
eventSink = ::handleEvents
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,5 +30,6 @@ data class RoomListState(
|
||||||
val displayVerificationPrompt: Boolean,
|
val displayVerificationPrompt: Boolean,
|
||||||
val hasNetworkConnection: Boolean,
|
val hasNetworkConnection: Boolean,
|
||||||
val snackbarMessage: SnackbarMessage?,
|
val snackbarMessage: SnackbarMessage?,
|
||||||
|
val displayInvites: Boolean,
|
||||||
val eventSink: (RoomListEvents) -> Unit
|
val eventSink: (RoomListEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ open class RoomListStateProvider : PreviewParameterProvider<RoomListState> {
|
||||||
aRoomListState().copy(displayVerificationPrompt = true),
|
aRoomListState().copy(displayVerificationPrompt = true),
|
||||||
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
aRoomListState().copy(snackbarMessage = SnackbarMessage(StringR.string.common_verification_complete)),
|
||||||
aRoomListState().copy(hasNetworkConnection = false),
|
aRoomListState().copy(hasNetworkConnection = false),
|
||||||
|
aRoomListState().copy(displayInvites = true),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -44,6 +45,7 @@ internal fun aRoomListState() = RoomListState(
|
||||||
hasNetworkConnection = true,
|
hasNetworkConnection = true,
|
||||||
snackbarMessage = null,
|
snackbarMessage = null,
|
||||||
displayVerificationPrompt = false,
|
displayVerificationPrompt = false,
|
||||||
|
displayInvites = false,
|
||||||
eventSink = {}
|
eventSink = {}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,18 +17,23 @@
|
||||||
package io.element.android.features.roomlist.impl
|
package io.element.android.features.roomlist.impl
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
|
@ -46,6 +51,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.stringResource
|
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.Scaffold
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
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.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.designsystem.utils.LogCompositions
|
||||||
import io.element.android.libraries.matrix.api.core.RoomId
|
import io.element.android.libraries.matrix.api.core.RoomId
|
||||||
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView
|
||||||
|
|
@ -81,6 +89,7 @@ fun RoomListView(
|
||||||
onOpenSettings: () -> Unit = {},
|
onOpenSettings: () -> Unit = {},
|
||||||
onVerifyClicked: () -> Unit = {},
|
onVerifyClicked: () -> Unit = {},
|
||||||
onCreateRoomClicked: () -> Unit = {},
|
onCreateRoomClicked: () -> Unit = {},
|
||||||
|
onInvitesClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
RoomListContent(
|
RoomListContent(
|
||||||
state = state,
|
state = state,
|
||||||
|
|
@ -89,6 +98,7 @@ fun RoomListView(
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onVerifyClicked = onVerifyClicked,
|
onVerifyClicked = onVerifyClicked,
|
||||||
onCreateRoomClicked = onCreateRoomClicked,
|
onCreateRoomClicked = onCreateRoomClicked,
|
||||||
|
onInvitesClicked = onInvitesClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,6 +111,7 @@ fun RoomListContent(
|
||||||
onRoomClicked: (RoomId) -> Unit = {},
|
onRoomClicked: (RoomId) -> Unit = {},
|
||||||
onOpenSettings: () -> Unit = {},
|
onOpenSettings: () -> Unit = {},
|
||||||
onCreateRoomClicked: () -> Unit = {},
|
onCreateRoomClicked: () -> Unit = {},
|
||||||
|
onInvitesClicked: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
fun onRoomClicked(room: RoomListRoomSummary) {
|
fun onRoomClicked(room: RoomListRoomSummary) {
|
||||||
onRoomClicked(room.roomId)
|
onRoomClicked(room.roomId)
|
||||||
|
|
@ -133,7 +144,7 @@ fun RoomListContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val snackbarMessageText = if (state.snackbarMessage != null ) {
|
val snackbarMessageText = if (state.snackbarMessage != null) {
|
||||||
stringResource(state.snackbarMessage.messageResId)
|
stringResource(state.snackbarMessage.messageResId)
|
||||||
} else null
|
} else null
|
||||||
val coroutineScope = rememberCoroutineScope()
|
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(
|
||||||
items = state.roomList,
|
items = state.roomList,
|
||||||
contentType = { room -> room.contentType() },
|
contentType = { room -> room.contentType() },
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
private fun createDateFormatter(): LastMessageTimestampFormatter {
|
||||||
return FakeLastMessageTimestampFormatter().apply {
|
return FakeLastMessageTimestampFormatter().apply {
|
||||||
givenFormat(A_FORMATTED_DATE)
|
givenFormat(A_FORMATTED_DATE)
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,7 @@ import java.io.Closeable
|
||||||
interface MatrixClient : Closeable {
|
interface MatrixClient : Closeable {
|
||||||
val sessionId: SessionId
|
val sessionId: SessionId
|
||||||
val roomSummaryDataSource: RoomSummaryDataSource
|
val roomSummaryDataSource: RoomSummaryDataSource
|
||||||
|
val invitesDataSource: RoomSummaryDataSource
|
||||||
fun getRoom(roomId: RoomId): MatrixRoom?
|
fun getRoom(roomId: RoomId): MatrixRoom?
|
||||||
fun findDM(userId: UserId): MatrixRoom?
|
fun findDM(userId: UserId): MatrixRoom?
|
||||||
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
suspend fun createRoom(createRoomParams: CreateRoomParameters): Result<RoomId>
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,11 @@ sealed interface RoomSummary {
|
||||||
data class RoomSummaryDetails(
|
data class RoomSummaryDetails(
|
||||||
val roomId: RoomId,
|
val roomId: RoomId,
|
||||||
val name: String,
|
val name: String,
|
||||||
|
val canonicalAlias: String? = null,
|
||||||
val isDirect: Boolean,
|
val isDirect: Boolean,
|
||||||
val avatarURLString: String?,
|
val avatarURLString: String?,
|
||||||
val lastMessage: RoomMessage?,
|
val lastMessage: RoomMessage?,
|
||||||
val lastMessageTimestamp: Long?,
|
val lastMessageTimestamp: Long?,
|
||||||
val unreadNotificationCount: Int,
|
val unreadNotificationCount: Int,
|
||||||
|
val inviter: RoomMember? = null,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -88,20 +88,18 @@ class RustMatrixClient constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val slidingSyncFilters by lazy {
|
private val visibleRoomsSlidingSyncFilters = SlidingSyncRequestListFilters(
|
||||||
SlidingSyncRequestListFilters(
|
isDm = null,
|
||||||
isDm = null,
|
spaces = emptyList(),
|
||||||
spaces = emptyList(),
|
isEncrypted = null,
|
||||||
isEncrypted = null,
|
isInvite = false,
|
||||||
isInvite = false,
|
isTombstoned = false,
|
||||||
isTombstoned = false,
|
roomTypes = emptyList(),
|
||||||
roomTypes = emptyList(),
|
notRoomTypes = listOf("m.space"),
|
||||||
notRoomTypes = listOf("m.space"),
|
roomNameLike = null,
|
||||||
roomNameLike = null,
|
tags = emptyList(),
|
||||||
tags = emptyList(),
|
notTags = emptyList()
|
||||||
notTags = emptyList()
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
|
private val visibleRoomsSlidingSyncList = SlidingSyncListBuilder()
|
||||||
.timelineLimit(limit = 1u)
|
.timelineLimit(limit = 1u)
|
||||||
|
|
@ -112,7 +110,7 @@ class RustMatrixClient constructor(
|
||||||
RequiredState(key = "m.room.join_rules", value = ""),
|
RequiredState(key = "m.room.join_rules", value = ""),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.filters(slidingSyncFilters)
|
.filters(visibleRoomsSlidingSyncFilters)
|
||||||
.name(name = "CurrentlyVisibleRooms")
|
.name(name = "CurrentlyVisibleRooms")
|
||||||
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
.syncMode(mode = SlidingSyncMode.SELECTIVE)
|
||||||
.addRange(0u, 20u)
|
.addRange(0u, 20u)
|
||||||
|
|
@ -120,12 +118,32 @@ class RustMatrixClient constructor(
|
||||||
it.build()
|
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
|
private val slidingSync = client
|
||||||
.slidingSync()
|
.slidingSync()
|
||||||
.homeserver("https://slidingsync.lab.matrix.org")
|
.homeserver("https://slidingsync.lab.matrix.org")
|
||||||
.withCommonExtensions()
|
.withCommonExtensions()
|
||||||
.storageKey("ElementX")
|
.storageKey("ElementX")
|
||||||
.addList(visibleRoomsSlidingSyncList)
|
.addList(visibleRoomsSlidingSyncList)
|
||||||
|
.addList(invitesSlidingSyncList)
|
||||||
.use {
|
.use {
|
||||||
it.build()
|
it.build()
|
||||||
}
|
}
|
||||||
|
|
@ -143,6 +161,18 @@ class RustMatrixClient constructor(
|
||||||
override val roomSummaryDataSource: RoomSummaryDataSource
|
override val roomSummaryDataSource: RoomSummaryDataSource
|
||||||
get() = rustRoomSummaryDataSource
|
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 var slidingSyncObserverToken: TaskHandle? = null
|
||||||
|
|
||||||
private val mediaResolver = RustMediaResolver(this)
|
private val mediaResolver = RustMediaResolver(this)
|
||||||
|
|
@ -153,6 +183,7 @@ class RustMatrixClient constructor(
|
||||||
init {
|
init {
|
||||||
client.setDelegate(clientDelegate)
|
client.setDelegate(clientDelegate)
|
||||||
rustRoomSummaryDataSource.init()
|
rustRoomSummaryDataSource.init()
|
||||||
|
rustInvitesDataSource.init()
|
||||||
slidingSync.setObserver(slidingSyncObserverProxy)
|
slidingSync.setObserver(slidingSyncObserverProxy)
|
||||||
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow
|
slidingSyncUpdateJob = slidingSyncObserverProxy.updateSummaryFlow
|
||||||
.onEach { onSlidingSyncUpdate() }
|
.onEach { onSlidingSyncUpdate() }
|
||||||
|
|
@ -249,6 +280,7 @@ class RustMatrixClient constructor(
|
||||||
stopSync()
|
stopSync()
|
||||||
slidingSync.setObserver(null)
|
slidingSync.setObserver(null)
|
||||||
rustRoomSummaryDataSource.close()
|
rustRoomSummaryDataSource.close()
|
||||||
|
rustInvitesDataSource.close()
|
||||||
client.setDelegate(null)
|
client.setDelegate(null)
|
||||||
visibleRoomsSlidingSyncList.destroy()
|
visibleRoomsSlidingSyncList.destroy()
|
||||||
slidingSync.destroy()
|
slidingSync.destroy()
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.room
|
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.core.RoomId
|
||||||
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
import io.element.android.libraries.matrix.api.room.RoomSummaryDetails
|
||||||
import io.element.android.libraries.matrix.impl.room.message.RoomMessageFactory
|
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 {
|
val latestRoomMessage = slidingSyncRoom.latestRoomMessage()?.use {
|
||||||
roomMessageFactory.create(it)
|
roomMessageFactory.create(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
return RoomSummaryDetails(
|
return RoomSummaryDetails(
|
||||||
roomId = RoomId(slidingSyncRoom.roomId()),
|
roomId = RoomId(slidingSyncRoom.roomId()),
|
||||||
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
name = slidingSyncRoom.name() ?: slidingSyncRoom.roomId(),
|
||||||
|
canonicalAlias = room?.canonicalAlias(),
|
||||||
isDirect = slidingSyncRoom.isDm() ?: false,
|
isDirect = slidingSyncRoom.isDm() ?: false,
|
||||||
avatarURLString = room?.avatarUrl(),
|
avatarURLString = room?.avatarUrl(),
|
||||||
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
unreadNotificationCount = slidingSyncRoom.unreadNotifications().use { it.notificationCount().toInt() },
|
||||||
lastMessage = latestRoomMessage,
|
lastMessage = latestRoomMessage,
|
||||||
lastMessageTimestamp = latestRoomMessage?.originServerTs
|
lastMessageTimestamp = latestRoomMessage?.originServerTs,
|
||||||
|
inviter = room?.inviter()?.let(RoomMemberMapper::map),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ class FakeMatrixClient(
|
||||||
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
|
private val userDisplayName: Result<String> = Result.success(A_USER_NAME),
|
||||||
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
|
private val userAvatarURLString: Result<String> = Result.success(AN_AVATAR_URL),
|
||||||
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
|
override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
|
||||||
|
override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(),
|
||||||
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(),
|
||||||
private val pushersService: FakePushersService = FakePushersService(),
|
private val pushersService: FakePushersService = FakePushersService(),
|
||||||
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
private val notificationService: FakeNotificationService = FakeNotificationService(),
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@
|
||||||
<string name="action_view_source">"View Source"</string>
|
<string name="action_view_source">"View Source"</string>
|
||||||
<string name="action_yes">"Yes"</string>
|
<string name="action_yes">"Yes"</string>
|
||||||
<string name="common_about">"About"</string>
|
<string name="common_about">"About"</string>
|
||||||
|
<string name="common_analytics">"Analytics"</string>
|
||||||
<string name="common_audio">"Audio"</string>
|
<string name="common_audio">"Audio"</string>
|
||||||
<string name="common_bubbles">"Bubbles"</string>
|
<string name="common_bubbles">"Bubbles"</string>
|
||||||
<string name="common_creating_room">"Creating room…"</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">"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_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
|
||||||
<string name="room_timeline_read_marker_title">"New"</string>
|
<string name="room_timeline_read_marker_title">"New"</string>
|
||||||
<string name="screen_invites_empty_list">"No Invites"</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_invites_invited_you">"%1$s invited you"</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">"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_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>
|
<string name="screen_room_member_details_block_alert_action">"Block"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae7f920795745910f0df3a472d31b055fed52a9041a0b7894f9a732be8777b2e
|
||||||
|
size 22344
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:52d02a23a34fbf2e3ba41547a10ecb774d8ef40940ef9cc30b79b98fbafe267e
|
||||||
|
size 25888
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e9622f36ef930ce3b81e656c0447d971b9c2939ae4b48268689bf1256a6c9249
|
||||||
|
size 16978
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5f94f5fac1646765a8b07712d046c7446c8c019384e727899178ef0bd8bd78fb
|
||||||
|
size 21545
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f0272e8fc322038a054a94b516a0c36136da0c1d244790d72cfde97de85845d0
|
||||||
|
size 25178
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5cfeafe4170c00c0344ec5542279e947eb0a7cc4d6558a4c79564d6fc2fb3bab
|
||||||
|
size 16241
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e4e085e53b63f83a51bb726fc3f38f37c8fb6412e5b8ed3ec893b24c87fa63a4
|
||||||
|
size 56723
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:bb46d54ea5b341f34284c5478045fae971004ef2c4208f1a57743868e2058c80
|
||||||
|
size 9312
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:151331de0ea941330ff611e312833aa9c731ed0cd49ed4f31f724dbb5c7ba252
|
||||||
|
size 55174
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:3f4ce908e84c489268907758d54a44711d84a05947d34177f6408b1a78f2f494
|
||||||
|
size 8850
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
|
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
|
||||||
size 6302
|
size 6303
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
|
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
|
||||||
size 7036
|
size 7035
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:712e7422e6f0ae9d6093619b156d982b0d4a861dc04c04473d3d0eb5751bd097
|
oid sha256:8db2a0def5cf23917ababd10121dda386ccc44aa0172810e9ca916fbb63b08d0
|
||||||
size 6302
|
size 6303
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7e3db033f79ac2c77abb8c41f6331e17ca89702fe00e42fa0b3c3f68b0d331de
|
oid sha256:417e4325a43fd13637f54fa71536441f2c4b5652e3bb25e2d579ef7d713c4aa9
|
||||||
size 7036
|
size 7035
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
|
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
|
||||||
size 6161
|
size 6163
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
|
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
|
||||||
size 6836
|
size 6834
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7cdecfdb401b135159f3a4f9001a7739e4ba9a8fd80371b4a68278a07a8834c0
|
oid sha256:593546b9837061cc2f0e27b94fe8a6fae80c35db62767cd43b97cc77c942c71b
|
||||||
size 6161
|
size 6163
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:310058557ae80956fe325fc8a58df7b62c45040d707a0f7569f87cbb74cede07
|
oid sha256:93805e6e1a168295b4263152f569b3b7065e890c11870f7e21f5b0f5cc1b07a6
|
||||||
size 6836
|
size 6834
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3dc781e3f17f09eb7e0cb8bf753a68ebde850b0b618e8d6bff7c780e365ee778
|
oid sha256:ff1f9f65252e855037d6008aea0d884ccaec6419412b26e4ad12a32ab583f5ca
|
||||||
size 11853
|
size 11848
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6d14b21a4723db7ac5ceda97eee44681d5d236007262e0e9e8b5a8d5abf8022b
|
oid sha256:37c370bc41527d7b407679c971a35aee051da93c033c2f195b6c3d6c7b2d885a
|
||||||
size 11514
|
size 11514
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:562e83b7771ff2209c7882180af55a8f3123c12c020b294858b1b46bff8a8a95
|
oid sha256:5315b975f8b9def0b0d7575160cb34300511f1b83845ee2d1762188481cf6fe9
|
||||||
size 30928
|
size 30927
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:79b16cbbbf36e6de4ea6270476371c7097424b26d4066f7156738c084fe061de
|
oid sha256:099401d03621aca301d3c4590d90ba4e4642d400d7603a8e15140a1bfef0c0ec
|
||||||
size 42758
|
size 42758
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7534d8379a1ab220833b8c877f9b3021a665cdb97c71ba383695276d83757702
|
oid sha256:ca5dbc3aa00d6116de38fc0217b20ab0db4096b1f76bcc18b43ab9fd343f8110
|
||||||
size 32721
|
size 32722
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a3123fce3d4e4ed0480d2757e4daa70ecd14ddad1e8471523638561cccbab8dc
|
oid sha256:6211ec07d10d48e4713206e4c1fe24eae890fe2310d5bcc1f4ca54cc57f84ec9
|
||||||
size 44568
|
size 44564
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:32d8032b1b2b3ae2b2795dea60b6395104df24790450f9452bbe82e81d9e62e8
|
oid sha256:8ac7985e70782a84fc351cb15536bfd700ca8d6d48d5da0ee9dcaf5c78226479
|
||||||
size 28911
|
size 28909
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:abff6db46d39ec39f3c0d5366f91252b58cbf62e6850bc86a6e81e52c80ba096
|
oid sha256:c99138940ed1a8beac7da3c673600016a03b87195e07f205d5d389ef05be0fb0
|
||||||
size 45979
|
size 45978
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:34469e4b86a441fb6f7cfbdce4b622109c093a234ae5d35d895dce35ef10c20c
|
oid sha256:2f24112ad89e5ef93917d45e3232043168d5092ce6d62a7d414773dd0afda963
|
||||||
size 30327
|
size 30326
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:11c677ab3124a94f531f1db14eb2b2f19a493470620f7375cf7c1d14056cdbc9
|
oid sha256:9c696c464515873a94d28f8909dc47255717fc3b012ce0d80000769714e3cb9c
|
||||||
size 42429
|
size 42427
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9a35878a3d306b1a6d1768669904ee1f9e0a71abd4ab3c04ff6564027732614f
|
oid sha256:0fe506e4e7adbfe19fd48f0a40c19343471e0bb06e12fac8d5e6d54595e7193a
|
||||||
size 32077
|
size 32076
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c1b3b0b538dafa85849c6e255bcff6b1a82a52d49be1fe4ded9b7f7c2ec06bff
|
oid sha256:c9c9374c303c9a71b0526fccfd18903d145c1b3b0059de3ccd41cf80f1541a80
|
||||||
size 44431
|
size 44427
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ef18369400f738e94c5a812dc0ec4c8473659eafb11d7361385647ba6a640d15
|
oid sha256:5f18d8b33eace857f78b92eed035621125bbfca1f0d43e5e47fd1c3965324de0
|
||||||
size 28320
|
size 28319
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:633210082fc83be3628250fa35768969aad136035924667bd2f3c6c8307fd87c
|
oid sha256:be0722a804c2fc264f931f8784635bceab7ddcf08e378bed8ea510bd76813fbf
|
||||||
size 45641
|
size 45642
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d6de6a4dee8a62839c15a84de8cb9817b3de8ae9fd2317e723c47bb679a72b7d
|
oid sha256:1298db69feb09f55f2c020a9e5262f8a462700d2bd481af1c2fa7f54069069f8
|
||||||
size 39603
|
size 39602
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8b1012920df3d9e6754e43464723139ae3fd0c04a518f761afcd54e25105acd6
|
oid sha256:4a6cf87a5664b751e96e9bc9741f0d7d78ce59b8805ef311d308fc6c2da694b7
|
||||||
size 41437
|
size 41436
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:caaf172ddb39cbd1a77bfb2295202e5c0aa95c846353a17da6abe6c50316cf63
|
oid sha256:75de58862f2801ce1d51e9f96fc68968a4c92e7caa9b71a1bcd7228905191990
|
||||||
size 38510
|
size 38513
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:59c0a62bf960d150ee111e88a932e9f5c85a82336a787b10db9f3c157ab13a35
|
oid sha256:b553c8482b5c7d129fc83dee9e0f9ff6926c9b31f19e0786281b9a8e71706472
|
||||||
size 40520
|
size 40522
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c820bd324df729db08710d1a6c17ca34a451a3bb94da2206fc57d6b4efe91e2f
|
oid sha256:b53d55b5085673ac3a0a7663f7ff68e7d510fe352f3931b49c7e75e4c3767b93
|
||||||
size 60019
|
size 60007
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:da2f9f87d89382b4b18d39a477a8e86f98067e2326adae4def8374b5bf09f316
|
oid sha256:a761318f3dfbc2ce6e777cfabc19eeb2f89100ae7631660f4b4d7550cd947c84
|
||||||
size 57588
|
size 57580
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9630d9b5e3d26b677eaa8c0496d76d73f95d28a487ef0ef845d3a89747d6a8cb
|
oid sha256:3c6371800946673d56bf06020f79a106f41474a4bc7664abbdf57c4228352611
|
||||||
size 179668
|
size 179661
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2f8ad5f69dbcda0284f731a01851fc73dc93ba9b1fad9e778b23f0155bfc7107
|
oid sha256:7feb6804ceca70ed0081ef36f23ba5623e263d05b0c12d5f08ba81c62f9ec492
|
||||||
size 178346
|
size 178343
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:2b1222e1ef2d0739caa410540bf74fea681ef2e05bb04b976e50f1fe5256d613
|
||||||
|
size 39762
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ebb15a58e5d3497b2819f5a6b6fde88962f01cc2ae02d4b8cda0e45e1dde677f
|
||||||
|
size 39314
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:6428b3de7e12b0eac27ab969605e35fed78dd3193998cd719387421330e91321
|
||||||
|
size 16396
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8e4e8e38986aa729d5de1384ea79620cdb9e01d001712c7a78d001ebe5bef623
|
||||||
|
size 15255
|
||||||
|
|
@ -25,6 +25,12 @@
|
||||||
"screen_onboarding_.*"
|
"screen_onboarding_.*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": ":features:invitelist:impl",
|
||||||
|
"includeRegex": [
|
||||||
|
"screen_invites_.*"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": ":features:createroom:impl",
|
"name": ":features:createroom:impl",
|
||||||
"includeRegex": [
|
"includeRegex": [
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue