Merge pull request #3995 from element-hq/feature/fga/requests_to_join_list
feat(knock_requests_list) : implement design
This commit is contained in:
commit
eae73ac2b9
49 changed files with 1031 additions and 20 deletions
19
features/knockrequests/api/build.gradle.kts
Normal file
19
features/knockrequests/api/build.gradle.kts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
plugins {
|
||||
id("io.element.android-library")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.knockrequests.api"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.api.list
|
||||
|
||||
import io.element.android.libraries.architecture.SimpleFeatureEntryPoint
|
||||
|
||||
interface KnockRequestsListEntryPoint : SimpleFeatureEntryPoint
|
||||
36
features/knockrequests/impl/build.gradle.kts
Normal file
36
features/knockrequests/impl/build.gradle.kts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import extension.setupAnvil
|
||||
|
||||
plugins {
|
||||
id("io.element.android-compose-library")
|
||||
id("kotlin-parcelize")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "io.element.android.features.knockrequests.impl"
|
||||
}
|
||||
|
||||
setupAnvil()
|
||||
|
||||
dependencies {
|
||||
api(projects.features.knockrequests.api)
|
||||
implementation(projects.libraries.core)
|
||||
implementation(projects.libraries.architecture)
|
||||
implementation(projects.libraries.matrix.api)
|
||||
implementation(projects.libraries.matrixui)
|
||||
implementation(projects.libraries.uiStrings)
|
||||
implementation(projects.libraries.designsystem)
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl
|
||||
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
|
||||
data class KnockRequest(
|
||||
val userId: UserId,
|
||||
val displayName: String?,
|
||||
val avatarUrl: String?,
|
||||
val reason: String?,
|
||||
val formattedDate: String?,
|
||||
)
|
||||
|
||||
fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
|
||||
id = userId.value,
|
||||
name = displayName,
|
||||
url = avatarUrl,
|
||||
size = size,
|
||||
)
|
||||
|
||||
fun KnockRequest.getBestName(): String {
|
||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
import com.bumble.appyx.core.node.Node
|
||||
import com.squareup.anvil.annotations.ContributesBinding
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.libraries.architecture.createNode
|
||||
import io.element.android.libraries.di.AppScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@ContributesBinding(AppScope::class)
|
||||
class DefaultKnockRequestsListEntryPoint @Inject constructor() : KnockRequestsListEntryPoint {
|
||||
override fun createNode(parentNode: Node, buildContext: BuildContext): Node {
|
||||
return parentNode.createNode<KnockRequestsListNode>(buildContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
|
||||
sealed interface KnockRequestsListEvents {
|
||||
data class Accept(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
||||
data class Decline(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
||||
data class DeclineAndBan(val knockRequest: KnockRequest) : KnockRequestsListEvents
|
||||
data object AcceptAll : KnockRequestsListEvents
|
||||
data object DismissCurrentAction : KnockRequestsListEvents
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
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 dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesNode(RoomScope::class)
|
||||
class KnockRequestsListNode @AssistedInject constructor(
|
||||
@Assisted buildContext: BuildContext,
|
||||
@Assisted plugins: List<Plugin>,
|
||||
private val presenter: KnockRequestsListPresenter,
|
||||
) : Node(buildContext, plugins = plugins) {
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
KnockRequestsListView(
|
||||
state = state,
|
||||
onBackClick = ::navigateUp,
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.ui.room.canBanAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canInviteAsState
|
||||
import io.element.android.libraries.matrix.ui.room.canKickAsState
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import javax.inject.Inject
|
||||
|
||||
class KnockRequestsListPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
) : Presenter<KnockRequestsListState> {
|
||||
@Composable
|
||||
override fun present(): KnockRequestsListState {
|
||||
val currentAction = remember { mutableStateOf<KnockRequestsCurrentAction>(KnockRequestsCurrentAction.None) }
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val canBan by room.canBanAsState(syncUpdateFlow.value)
|
||||
val canDecline by room.canKickAsState(syncUpdateFlow.value)
|
||||
val canAccept by room.canInviteAsState(syncUpdateFlow.value)
|
||||
|
||||
fun handleEvents(event: KnockRequestsListEvents) {
|
||||
when (event) {
|
||||
KnockRequestsListEvents.AcceptAll -> {
|
||||
currentAction.value = KnockRequestsCurrentAction.AcceptAll(AsyncAction.Uninitialized)
|
||||
}
|
||||
is KnockRequestsListEvents.Accept -> {
|
||||
currentAction.value = KnockRequestsCurrentAction.Accept(event.knockRequest, AsyncAction.Uninitialized)
|
||||
}
|
||||
is KnockRequestsListEvents.Decline -> {
|
||||
currentAction.value = KnockRequestsCurrentAction.Decline(event.knockRequest, AsyncAction.Uninitialized)
|
||||
}
|
||||
is KnockRequestsListEvents.DeclineAndBan -> {
|
||||
currentAction.value = KnockRequestsCurrentAction.DeclineAndBan(event.knockRequest, AsyncAction.Uninitialized)
|
||||
}
|
||||
KnockRequestsListEvents.DismissCurrentAction -> {
|
||||
currentAction.value = KnockRequestsCurrentAction.None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentAction) {
|
||||
when (currentAction.value) {
|
||||
is KnockRequestsCurrentAction.Accept -> {
|
||||
// Accept the knock request
|
||||
}
|
||||
is KnockRequestsCurrentAction.Decline -> {
|
||||
// Decline the knock request
|
||||
}
|
||||
is KnockRequestsCurrentAction.DeclineAndBan -> {
|
||||
// Decline and ban the user
|
||||
}
|
||||
is KnockRequestsCurrentAction.AcceptAll -> {
|
||||
// Accept all knock requests
|
||||
}
|
||||
KnockRequestsCurrentAction.None -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
return KnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(persistentListOf()),
|
||||
currentAction = currentAction.value,
|
||||
canAccept = canAccept,
|
||||
canDecline = canDecline,
|
||||
canBan = canBan,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
data class KnockRequestsListState(
|
||||
val knockRequests: AsyncData<ImmutableList<KnockRequest>>,
|
||||
val currentAction: KnockRequestsCurrentAction,
|
||||
val canAccept: Boolean,
|
||||
val canDecline: Boolean,
|
||||
val canBan: Boolean,
|
||||
val eventSink: (KnockRequestsListEvents) -> Unit,
|
||||
) {
|
||||
val canAcceptAll = knockRequests is AsyncData.Success && knockRequests.data.size > 1
|
||||
}
|
||||
|
||||
@Immutable
|
||||
sealed interface KnockRequestsCurrentAction {
|
||||
data object None : KnockRequestsCurrentAction
|
||||
data class Accept(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
||||
data class Decline(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
||||
data class DeclineAndBan(val knockRequest: KnockRequest, val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
||||
data class AcceptAll(val async: AsyncAction<Unit>) : KnockRequestsCurrentAction
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockRequestsListState> {
|
||||
override val values: Sequence<KnockRequestsListState>
|
||||
get() = sequenceOf(
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Loading(),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf()
|
||||
),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest(
|
||||
reason = "A very long reason that should probably be truncated, " +
|
||||
"but could be also expanded so you can see it over the lines, wow," +
|
||||
"very amazing reason, I know, right, I'm so good at writing reasons."
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest(),
|
||||
aKnockRequest(
|
||||
userId = UserId("@user:example.com"),
|
||||
displayName = null,
|
||||
avatarUrl = null,
|
||||
reason = null,
|
||||
)
|
||||
)
|
||||
),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
currentAction = KnockRequestsCurrentAction.AcceptAll(AsyncAction.Loading),
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
canAccept = false,
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
canDecline = false,
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
canAccept = false,
|
||||
canDecline = false,
|
||||
),
|
||||
aKnockRequestsListState(
|
||||
knockRequests = AsyncData.Success(
|
||||
persistentListOf(
|
||||
aKnockRequest()
|
||||
)
|
||||
),
|
||||
canBan = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aKnockRequest(
|
||||
userId: UserId = UserId("@jacob_ross:example.com"),
|
||||
displayName: String? = "Jacob Ross",
|
||||
avatarUrl: String? = null,
|
||||
reason: String? = "Hi, I would like to get access to this room please.",
|
||||
formattedDate: String = "20 Nov 2024",
|
||||
) = KnockRequest(
|
||||
userId = userId,
|
||||
displayName = displayName,
|
||||
avatarUrl = avatarUrl,
|
||||
reason = reason,
|
||||
formattedDate = formattedDate,
|
||||
)
|
||||
|
||||
fun aKnockRequestsListState(
|
||||
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
||||
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
||||
canAccept: Boolean = true,
|
||||
canDecline: Boolean = true,
|
||||
canBan: Boolean = true,
|
||||
eventSink: (KnockRequestsListEvents) -> Unit = {},
|
||||
) = KnockRequestsListState(
|
||||
knockRequests = knockRequests,
|
||||
currentAction = currentAction,
|
||||
canAccept = canAccept,
|
||||
canDecline = canDecline,
|
||||
canBan = canBan,
|
||||
eventSink = eventSink,
|
||||
)
|
||||
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.knockrequests.impl.list
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clipToBounds
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||
import io.element.android.features.knockrequests.impl.R
|
||||
import io.element.android.features.knockrequests.impl.getAvatarData
|
||||
import io.element.android.features.knockrequests.impl.getBestName
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
import io.element.android.libraries.designsystem.components.async.AsyncActionView
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.theme.aliasScreenTitle
|
||||
import io.element.android.libraries.designsystem.theme.components.Button
|
||||
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||
import io.element.android.libraries.designsystem.theme.components.HorizontalDivider
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||
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.components.TopAppBar
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
@Composable
|
||||
fun KnockRequestsListView(
|
||||
state: KnockRequestsListState,
|
||||
onBackClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
KnockRequestsListTopBar(onBackClick = onBackClick)
|
||||
},
|
||||
content = { padding ->
|
||||
KnockRequestsListContent(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.consumeWindowInsets(padding),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsListContent(
|
||||
state: KnockRequestsListState,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
fun onAcceptClick(knockRequest: KnockRequest) {
|
||||
state.eventSink(KnockRequestsListEvents.Accept(knockRequest))
|
||||
}
|
||||
|
||||
fun onDeclineClick(knockRequest: KnockRequest) {
|
||||
state.eventSink(KnockRequestsListEvents.Decline(knockRequest))
|
||||
}
|
||||
|
||||
var bottomPaddingInPixels by remember { mutableIntStateOf(0) }
|
||||
|
||||
Box(modifier.fillMaxSize()) {
|
||||
when (state.knockRequests) {
|
||||
is AsyncData.Success -> {
|
||||
val knockRequests = state.knockRequests.data
|
||||
if (knockRequests.isEmpty()) {
|
||||
KnockRequestsEmptyList()
|
||||
} else {
|
||||
KnockRequestsList(
|
||||
knockRequests = knockRequests,
|
||||
canAccept = state.canAccept,
|
||||
canDecline = state.canDecline,
|
||||
canBan = state.canBan,
|
||||
onAcceptClick = ::onAcceptClick,
|
||||
onDeclineClick = ::onDeclineClick,
|
||||
contentPadding = PaddingValues(bottom = bottomPaddingInPixels.toDp()),
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
KnockRequestsActionsView(
|
||||
actions = state.currentAction,
|
||||
onDismiss = {
|
||||
state.eventSink(KnockRequestsListEvents.DismissCurrentAction)
|
||||
},
|
||||
)
|
||||
if (state.canAcceptAll) {
|
||||
KnockRequestsAcceptAll(
|
||||
onClick = {
|
||||
state.eventSink(KnockRequestsListEvents.AcceptAll)
|
||||
},
|
||||
onHeightChange = { height ->
|
||||
bottomPaddingInPixels = height
|
||||
},
|
||||
modifier = Modifier.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsActionsView(
|
||||
actions: KnockRequestsCurrentAction,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier) {
|
||||
when (actions) {
|
||||
is KnockRequestsCurrentAction.AcceptAll -> {
|
||||
AsyncActionView(
|
||||
async = actions.async,
|
||||
onSuccess = {},
|
||||
onErrorDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
is KnockRequestsCurrentAction.Accept -> {
|
||||
AsyncActionView(
|
||||
async = actions.async,
|
||||
onSuccess = {},
|
||||
onErrorDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
is KnockRequestsCurrentAction.Decline -> {
|
||||
AsyncActionView(
|
||||
async = actions.async,
|
||||
onSuccess = {},
|
||||
onErrorDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
is KnockRequestsCurrentAction.DeclineAndBan -> {
|
||||
AsyncActionView(
|
||||
async = actions.async,
|
||||
onSuccess = {},
|
||||
onErrorDismiss = onDismiss,
|
||||
)
|
||||
}
|
||||
KnockRequestsCurrentAction.None -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsList(
|
||||
knockRequests: ImmutableList<KnockRequest>,
|
||||
canAccept: Boolean,
|
||||
canDecline: Boolean,
|
||||
canBan: Boolean,
|
||||
onAcceptClick: (KnockRequest) -> Unit,
|
||||
onDeclineClick: (KnockRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentPadding = contentPadding,
|
||||
) {
|
||||
itemsIndexed(knockRequests) { index, knockRequest ->
|
||||
KnockRequestItem(
|
||||
knockRequest = knockRequest,
|
||||
onAcceptClick = onAcceptClick,
|
||||
canBan = canBan,
|
||||
canDecline = canDecline,
|
||||
canAccept = canAccept,
|
||||
onDeclineClick = onDeclineClick,
|
||||
)
|
||||
if (index != knockRequests.size - 1) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestItem(
|
||||
knockRequest: KnockRequest,
|
||||
canAccept: Boolean,
|
||||
canDecline: Boolean,
|
||||
canBan: Boolean,
|
||||
onAcceptClick: (KnockRequest) -> Unit,
|
||||
onDeclineClick: (KnockRequest) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp)
|
||||
) {
|
||||
Avatar(knockRequest.getAvatarData(AvatarSize.KnockRequestItem))
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column {
|
||||
// Name and date
|
||||
Row {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clipToBounds()
|
||||
.weight(1f),
|
||||
text = knockRequest.getBestName(),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
)
|
||||
if (!knockRequest.formattedDate.isNullOrEmpty()) {
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = knockRequest.formattedDate,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
)
|
||||
}
|
||||
}
|
||||
// UserId
|
||||
if (!knockRequest.displayName.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = knockRequest.userId.value,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
)
|
||||
}
|
||||
// Reason
|
||||
if (!knockRequest.reason.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
var isExpanded by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
||||
var isExpandable by rememberSaveable(knockRequest.userId) { mutableStateOf(false) }
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.clickable(enabled = isExpandable) { isExpanded = !isExpanded }
|
||||
) {
|
||||
Text(
|
||||
text = knockRequest.reason,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
maxLines = if (isExpanded) Int.MAX_VALUE else 3,
|
||||
onTextLayout = { result ->
|
||||
if (!isExpanded && result.hasVisualOverflow) {
|
||||
isExpandable = true
|
||||
}
|
||||
},
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Box(modifier = Modifier.size(24.dp)) {
|
||||
if (isExpandable) {
|
||||
Icon(
|
||||
imageVector = if (isExpanded) CompoundIcons.ChevronUp() else CompoundIcons.ChevronDown(),
|
||||
contentDescription = null,
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Actions
|
||||
if (canDecline || canAccept) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
if (canDecline) {
|
||||
OutlinedButton(
|
||||
text = stringResource(CommonStrings.action_decline),
|
||||
onClick = {
|
||||
onDeclineClick(knockRequest)
|
||||
},
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
if (canAccept) {
|
||||
Button(
|
||||
text = stringResource(CommonStrings.action_accept),
|
||||
onClick = {
|
||||
onAcceptClick(knockRequest)
|
||||
},
|
||||
size = ButtonSize.MediumLowPadding,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (canBan) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TextButton(
|
||||
text = stringResource(R.string.screen_knock_requests_list_decline_and_ban_action_title),
|
||||
onClick = {
|
||||
onAcceptClick(knockRequest)
|
||||
},
|
||||
destructive = true,
|
||||
size = ButtonSize.Small,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsAcceptAll(
|
||||
onClick: () -> Unit,
|
||||
onHeightChange: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.shadow(elevation = 24.dp, spotColor = Color.Transparent)
|
||||
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||
.padding(vertical = 12.dp, horizontal = 16.dp)
|
||||
.onSizeChanged { onHeightChange(it.height) }
|
||||
) {
|
||||
OutlinedButton(
|
||||
text = stringResource(R.string.screen_knock_requests_list_accept_all_button_title),
|
||||
onClick = onClick,
|
||||
size = ButtonSize.Medium,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsEmptyList(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.padding(
|
||||
horizontal = 32.dp,
|
||||
vertical = 48.dp,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
IconTitleSubtitleMolecule(
|
||||
title = stringResource(R.string.screen_knock_requests_list_empty_state_title),
|
||||
subTitle = stringResource(R.string.screen_knock_requests_list_empty_state_description),
|
||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Pin()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun KnockRequestsListTopBar(onBackClick: () -> Unit) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.screen_knock_requests_list_title),
|
||||
style = ElementTheme.typography.aliasScreenTitle,
|
||||
)
|
||||
},
|
||||
navigationIcon = { BackButton(onClick = onBackClick) },
|
||||
)
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun KnockRequestsListViewPreview(
|
||||
@PreviewParameter(KnockRequestsListStateProvider::class) state: KnockRequestsListState
|
||||
) = ElementPreview {
|
||||
KnockRequestsListView(
|
||||
state = state,
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
17
features/knockrequests/impl/src/main/res/values/localazy.xml
Normal file
17
features/knockrequests/impl/src/main/res/values/localazy.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Yes, accept all"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Are you sure you want to accept all requests to join?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Accept all requests"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Accept all"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Yes, decline and ban"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Are you sure you want to decline and ban %1$s? This user won’t be able to request access to join this room again."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Decline and ban from accessing"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Yes, decline"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Are you sure you want to decline %1$s request to join this room?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Decline access"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Decline and ban"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"When somebody will ask to join the room, you’ll be able to see their request here."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"No pending request to join"</string>
|
||||
<string name="screen_knock_requests_list_title">"Requests to join"</string>
|
||||
</resources>
|
||||
|
|
@ -50,6 +50,7 @@ dependencies {
|
|||
implementation(projects.features.poll.api)
|
||||
implementation(projects.features.messages.api)
|
||||
implementation(projects.features.roomcall.api)
|
||||
implementation(projects.features.knockrequests.api)
|
||||
|
||||
testImplementation(libs.test.junit)
|
||||
testImplementation(libs.coroutines.test)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
|||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.features.call.api.CallType
|
||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||
import io.element.android.features.messages.api.MessagesEntryPoint
|
||||
import io.element.android.features.poll.api.history.PollHistoryEntryPoint
|
||||
import io.element.android.features.roomdetails.api.RoomDetailsEntryPoint
|
||||
|
|
@ -56,6 +57,7 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
private val room: MatrixRoom,
|
||||
private val analyticsService: AnalyticsService,
|
||||
private val messagesEntryPoint: MessagesEntryPoint,
|
||||
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||
private val mediaViewerEntryPoint: MediaViewerEntryPoint,
|
||||
) : BaseFlowNode<RoomDetailsFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
|
|
@ -101,6 +103,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
|
||||
@Parcelize
|
||||
data object PinnedMessagesList : NavTarget
|
||||
|
||||
@Parcelize
|
||||
data object KnockRequestsList : NavTarget
|
||||
}
|
||||
|
||||
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
|
||||
|
|
@ -139,6 +144,10 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
backstack.push(NavTarget.PinnedMessagesList)
|
||||
}
|
||||
|
||||
override fun openKnockRequestsList() {
|
||||
backstack.push(NavTarget.KnockRequestsList)
|
||||
}
|
||||
|
||||
override fun onJoinCall() {
|
||||
val inputs = CallType.RoomCall(
|
||||
sessionId = room.sessionId,
|
||||
|
|
@ -243,6 +252,9 @@ class RoomDetailsFlowNode @AssistedInject constructor(
|
|||
.callback(callback)
|
||||
.build()
|
||||
}
|
||||
NavTarget.KnockRequestsList -> {
|
||||
knockRequestsListEntryPoint.createNode(this, buildContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
fun openPollHistory()
|
||||
fun openAdminSettings()
|
||||
fun openPinnedMessagesList()
|
||||
fun openKnockRequestsList()
|
||||
fun onJoinCall()
|
||||
}
|
||||
|
||||
|
|
@ -111,6 +112,10 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
callbacks.forEach { it.openPinnedMessagesList() }
|
||||
}
|
||||
|
||||
private fun openKnockRequestsLists() {
|
||||
callbacks.forEach { it.openKnockRequestsList() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -140,7 +145,8 @@ class RoomDetailsNode @AssistedInject constructor(
|
|||
openPollHistory = ::openPollHistory,
|
||||
openAdminSettings = this::openAdminSettings,
|
||||
onJoinCallClick = ::onJoinCall,
|
||||
onPinnedMessagesClick = ::openPinnedMessages
|
||||
onPinnedMessagesClick = ::openPinnedMessages,
|
||||
onKnockRequestsClick = ::openKnockRequestsLists,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ import io.element.android.libraries.matrix.api.room.isDm
|
|||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canSendState
|
||||
import io.element.android.libraries.matrix.api.room.roomNotificationSettings
|
||||
import io.element.android.libraries.matrix.ui.room.canHandleKnockRequestsAsState
|
||||
import io.element.android.libraries.matrix.ui.room.getCurrentRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.getDirectRoomMember
|
||||
import io.element.android.libraries.matrix.ui.room.isOwnUserAdmin
|
||||
|
|
@ -69,7 +70,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val canShowNotificationSettings = remember { mutableStateOf(false) }
|
||||
val roomInfo by room.roomInfoFlow.collectAsState(initial = null)
|
||||
val isUserAdmin = room.isOwnUserAdmin()
|
||||
|
||||
val syncUpdateFlow = room.syncUpdateFlow.collectAsState()
|
||||
val roomAvatar by remember { derivedStateOf { roomInfo?.avatarUrl ?: room.avatarUrl } }
|
||||
|
||||
val roomName by remember { derivedStateOf { (roomInfo?.name ?: room.displayName).trim() } }
|
||||
|
|
@ -90,6 +91,7 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
|
||||
val membersState by room.membersStateFlow.collectAsState()
|
||||
val canInvite by getCanInvite(membersState)
|
||||
|
||||
val canEditName by getCanSendState(membersState, StateEventType.ROOM_NAME)
|
||||
val canEditAvatar by getCanSendState(membersState, StateEventType.ROOM_AVATAR)
|
||||
val canEditTopic by getCanSendState(membersState, StateEventType.ROOM_TOPIC)
|
||||
|
|
@ -99,6 +101,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
val roomType by getRoomType(dmMember, currentMember)
|
||||
val roomCallState = roomCallStatePresenter.present()
|
||||
|
||||
val canHandleKnockRequests by room.canHandleKnockRequestsAsState(syncUpdateFlow.value)
|
||||
|
||||
val topicState = remember(canEditTopic, roomTopic, roomType) {
|
||||
val topic = roomTopic
|
||||
|
||||
|
|
@ -109,6 +113,12 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
val isKnockRequestsEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.Knock).collectAsState(false)
|
||||
val knockRequestsCount by remember { mutableStateOf(null) }
|
||||
val canShowKnockRequests by remember {
|
||||
derivedStateOf { isKnockRequestsEnabled && canHandleKnockRequests }
|
||||
}
|
||||
|
||||
val roomNotificationSettingsState by room.roomNotificationSettingsStateFlow.collectAsState()
|
||||
|
||||
fun handleEvents(event: RoomDetailsEvent) {
|
||||
|
|
@ -153,6 +163,8 @@ class RoomDetailsPresenter @Inject constructor(
|
|||
heroes = roomInfo?.heroes.orEmpty().toPersistentList(),
|
||||
canShowPinnedMessages = canShowPinnedMessages,
|
||||
pinnedMessagesCount = pinnedMessagesCount,
|
||||
canShowKnockRequests = canShowKnockRequests,
|
||||
knockRequestsCount = knockRequestsCount,
|
||||
eventSink = ::handleEvents,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ data class RoomDetailsState(
|
|||
val heroes: ImmutableList<MatrixUser>,
|
||||
val canShowPinnedMessages: Boolean,
|
||||
val pinnedMessagesCount: Int?,
|
||||
val canShowKnockRequests: Boolean,
|
||||
val knockRequestsCount: Int?,
|
||||
val eventSink: (RoomDetailsEvent) -> Unit
|
||||
) {
|
||||
val roomBadges = buildList {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,8 @@ fun aRoomDetailsState(
|
|||
heroes: List<MatrixUser> = emptyList(),
|
||||
canShowPinnedMessages: Boolean = true,
|
||||
pinnedMessagesCount: Int? = null,
|
||||
canShowKnockRequests: Boolean = false,
|
||||
knockRequestsCount: Int? = null,
|
||||
eventSink: (RoomDetailsEvent) -> Unit = {},
|
||||
) = RoomDetailsState(
|
||||
roomId = roomId,
|
||||
|
|
@ -125,6 +127,8 @@ fun aRoomDetailsState(
|
|||
heroes = heroes.toPersistentList(),
|
||||
canShowPinnedMessages = canShowPinnedMessages,
|
||||
pinnedMessagesCount = pinnedMessagesCount,
|
||||
canShowKnockRequests = canShowKnockRequests,
|
||||
knockRequestsCount = knockRequestsCount,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ fun RoomDetailsView(
|
|||
openAdminSettings: () -> Unit,
|
||||
onJoinCallClick: () -> Unit,
|
||||
onPinnedMessagesClick: () -> Unit,
|
||||
onKnockRequestsClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Scaffold(
|
||||
|
|
@ -206,6 +207,12 @@ fun RoomDetailsView(
|
|||
memberCount = state.memberCount,
|
||||
openRoomMemberList = openRoomMemberList,
|
||||
)
|
||||
if (state.canShowKnockRequests) {
|
||||
KnockRequestsItem(
|
||||
knockRequestsCount = state.knockRequestsCount,
|
||||
onKnockRequestsClick = onKnockRequestsClick
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +238,20 @@ fun RoomDetailsView(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: () -> Unit) {
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_requests_to_join_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
||||
trailingContent = if (knockRequestsCount == null || knockRequestsCount == 0) {
|
||||
null
|
||||
} else {
|
||||
ListItemContent.Text(knockRequestsCount.toString())
|
||||
},
|
||||
onClick = onKnockRequestsClick,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun RoomDetailsTopBar(
|
||||
|
|
@ -525,7 +546,7 @@ private fun PinnedMessagesItem(
|
|||
) {
|
||||
val analyticsService = LocalAnalyticsService.current
|
||||
ListItem(
|
||||
headlineContent = { Text(stringResource(CommonStrings.screen_room_details_pinned_events_row_title)) },
|
||||
headlineContent = { Text(stringResource(R.string.screen_room_details_pinned_events_row_title)) },
|
||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Pin())),
|
||||
trailingContent =
|
||||
if (pinnedMessagesCount == null) {
|
||||
|
|
@ -613,5 +634,6 @@ private fun ContentToPreview(state: RoomDetailsState) {
|
|||
openAdminSettings = {},
|
||||
onJoinCallClick = {},
|
||||
onPinnedMessagesClick = {},
|
||||
onKnockRequestsClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,9 +49,12 @@
|
|||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||
<string name="screen_room_details_leave_conversation_title">"Leave conversation"</string>
|
||||
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
||||
<string name="screen_room_details_media_gallery_title">"Media and files"</string>
|
||||
<string name="screen_room_details_notification_mode_custom">"Custom"</string>
|
||||
<string name="screen_room_details_notification_mode_default">"Default"</string>
|
||||
<string name="screen_room_details_notification_title">"Notifications"</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
|
||||
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
|
||||
<string name="screen_room_details_roles_and_permissions">"Roles and permissions"</string>
|
||||
<string name="screen_room_details_room_name_label">"Room name"</string>
|
||||
<string name="screen_room_details_security_title">"Security"</string>
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@ class RoomDetailsViewTest {
|
|||
),
|
||||
onPinnedMessagesClick = callback,
|
||||
)
|
||||
rule.clickOn(CommonStrings.screen_room_details_pinned_events_row_title)
|
||||
rule.clickOn(R.string.screen_room_details_pinned_events_row_title)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -253,6 +253,21 @@ class RoomDetailsViewTest {
|
|||
rule.clickOn(R.string.screen_room_details_leave_room_title)
|
||||
eventsRecorder.assertSingle(RoomDetailsEvent.LeaveRoom)
|
||||
}
|
||||
|
||||
@Config(qualifiers = "h1024dp")
|
||||
@Test
|
||||
fun `click on knock requests invokes expected callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setRoomDetailView(
|
||||
state = aRoomDetailsState(
|
||||
eventSink = EventsRecorder(expectEvents = false),
|
||||
canShowKnockRequests = true,
|
||||
),
|
||||
onKnockRequestsClick = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_room_details_requests_to_join_title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomDetailView(
|
||||
|
|
@ -270,6 +285,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
openAdminSettings: () -> Unit = EnsureNeverCalled(),
|
||||
onJoinCallClick: () -> Unit = EnsureNeverCalled(),
|
||||
onPinnedMessagesClick: () -> Unit = EnsureNeverCalled(),
|
||||
onKnockRequestsClick: () -> Unit = EnsureNeverCalled(),
|
||||
) {
|
||||
setContent {
|
||||
RoomDetailsView(
|
||||
|
|
@ -285,6 +301,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setRoomD
|
|||
openAdminSettings = openAdminSettings,
|
||||
onJoinCallClick = onJoinCallClick,
|
||||
onPinnedMessagesClick = onPinnedMessagesClick,
|
||||
onKnockRequestsClick = onKnockRequestsClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,4 +54,6 @@ enum class AvatarSize(val dp: Dp) {
|
|||
EditProfileDetails(96.dp),
|
||||
|
||||
Suggestion(32.dp),
|
||||
|
||||
KnockRequestItem(52.dp),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,13 @@ suspend fun MatrixRoom.canRedactOwn(): Result<Boolean> = canUserRedactOwn(sessio
|
|||
*/
|
||||
suspend fun MatrixRoom.canRedactOther(): Result<Boolean> = canUserRedactOther(sessionId)
|
||||
|
||||
/**
|
||||
* Shortcut for checking if current user can handle knock requests.
|
||||
*/
|
||||
suspend fun MatrixRoom.canHandleKnockRequests(): Result<Boolean> = runCatching {
|
||||
canInvite().getOrThrow() || canBan().getOrThrow() || canKick().getOrThrow()
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut for calling [MatrixRoom.canUserPinUnpin] with our own user.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType
|
|||
import io.element.android.libraries.matrix.api.room.RoomMember
|
||||
import io.element.android.libraries.matrix.api.room.isDm
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canBan
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canHandleKnockRequests
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canInvite
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canKick
|
||||
import io.element.android.libraries.matrix.api.room.powerlevels.canRedactOther
|
||||
|
|
@ -86,6 +87,13 @@ fun MatrixRoom.canBanAsState(updateKey: Long): State<Boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.canHandleKnockRequestsAsState(updateKey: Long): State<Boolean> {
|
||||
return produceState(initialValue = false, key1 = updateKey) {
|
||||
value = canHandleKnockRequests().getOrElse { false }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MatrixRoom.userPowerLevelAsState(updateKey: Long): State<Long> {
|
||||
return produceState(initialValue = 0, key1 = updateKey) {
|
||||
|
|
|
|||
|
|
@ -299,20 +299,6 @@ Reason: %1$s."</string>
|
|||
<string name="invite_friends_text">"Hey, talk to me on %1$s: %2$s"</string>
|
||||
<string name="login_initial_device_name_android">"%1$s Android"</string>
|
||||
<string name="preference_rageshake">"Rageshake to report bug"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_confirm_button_title">"Yes, accept all"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_description">"Are you sure you want to accept all requests to join?"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_alert_title">"Accept all requests"</string>
|
||||
<string name="screen_knock_requests_list_accept_all_button_title">"Accept all"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_confirm_button_title">"Yes, decline and ban"</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_description">"Are you sure you want to decline and ban %1$s? This user won’t be able to request access to join this room again."</string>
|
||||
<string name="screen_knock_requests_list_ban_alert_title">"Decline and ban from accessing"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_confirm_button_title">"Yes, decline"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_description">"Are you sure you want to decline %1$s request to join this room?"</string>
|
||||
<string name="screen_knock_requests_list_decline_alert_title">"Decline access"</string>
|
||||
<string name="screen_knock_requests_list_decline_and_ban_action_title">"Decline and ban"</string>
|
||||
<string name="screen_knock_requests_list_empty_state_description">"When somebody will ask to join the room, you’ll be able to see their request here."</string>
|
||||
<string name="screen_knock_requests_list_empty_state_title">"No pending request to join"</string>
|
||||
<string name="screen_knock_requests_list_title">"Requests to join"</string>
|
||||
<string name="screen_media_picker_error_failed_selection">"Failed selecting media, please try again."</string>
|
||||
<string name="screen_media_upload_preview_caption_warning">"Captions might not be visible to people using older apps."</string>
|
||||
<string name="screen_media_upload_preview_error_failed_processing">"Failed processing media to upload, please try again."</string>
|
||||
|
|
@ -334,8 +320,6 @@ Reason: %1$s."</string>
|
|||
<string name="screen_resolve_send_failure_unsigned_device_title">"Your message was not sent because %1$s has not verified all devices"</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_subtitle">"One or more of your devices are unverified. You can send the message anyway, or you can cancel for now and try again later after you have verified all of your devices."</string>
|
||||
<string name="screen_resolve_send_failure_you_unsigned_device_title">"Your message was not sent because you have not verified one or more of your devices"</string>
|
||||
<string name="screen_room_details_pinned_events_row_title">"Pinned messages"</string>
|
||||
<string name="screen_room_details_requests_to_join_title">"Requests to join"</string>
|
||||
<string name="screen_room_error_failed_processing_media">"Failed processing media to upload, please try again."</string>
|
||||
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||
<plurals name="screen_room_multiple_knock_requests_title">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:fbbd687e5e0a1fd3a1be442937e32990dc72a6e4817d6ffc29f6f596eeddef1b
|
||||
size 8086
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a9595edf8183796a0a0efa838a3c5265e07f70452933e6255d79f536cfca64ac
|
||||
size 26138
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:a71bcfe210fdc6b2a0c54fe187d9528811bd6b5d442a66a1151b7a00ae514273
|
||||
size 33141
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:036a4f8f2f478df0e1705ff4bfd04599f6c0251936d2a80d750c9055d9d63ae0
|
||||
size 41550
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:99f070f9b5bdea3aa057776b303be171daff36f684770174f46b37ecc0eea781
|
||||
size 52135
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f7e66d3f7d10e759f629795580545cda6644292020ce36763d07e9d476dcc231
|
||||
size 30325
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:586021e67718aa949b1de04799a43d9da8189fadacdb6dba23405c762fc7ee06
|
||||
size 30618
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1b71dc6dc0d29801e81d1d4e52df811624583f69c0510f2f650676747a326455
|
||||
size 30296
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:f6be2cfe7991bce6c622f4f2e45f4ac96810cdc9462d9a4d7aea11baf69ab5ac
|
||||
size 27446
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:9afb58cfcd13c064f0b17d449318d5111aaa914cb8875fe0596c66e5a8a17051
|
||||
size 30647
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7ff4bc9e0588ea6a8396572b9ac9fb2401c3193fc3f1fbeb75efd69be6dda24d
|
||||
size 7867
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:37e24295a404463010f6d36ab94b1ac3c2e193d9122637abbeb0a43abcb427c1
|
||||
size 25669
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:7548a1029b4e63722aca769800728c0bb519d3d736cb54138bc5e053a1aca46e
|
||||
size 32150
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3b094584e67852da5ecb74511143943d3526965a548a24d5bb4e23540fc77b90
|
||||
size 40527
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:79e34ab35d9bd44d6e7694df882995f401f49a2ba042c6a3b2c010c12657750f
|
||||
size 50637
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:c30b6e25ec148d4f98a6c305b7d71af6c7e413cc3425f5a6f5b42a201039f491
|
||||
size 29007
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:e5c6abc8bd5ca40eb91d926adc798cee95a8aacb0e87d937983c6240173fe2b9
|
||||
size 30071
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3b67c8d2cedb724ece75a2171463c4e035d562c4480d39b11b1072f4fdae3053
|
||||
size 29840
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:15903c9494100becc4dd2bc6e9fb7f38f8eebbb595cbc251cfc0f839d9a99a27
|
||||
size 27322
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:68294db4cbd8bb42f4a0b6534b48585341beb2bc405e93cf74a2655f049522ff
|
||||
size 29768
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:66c29b560708bb2d8870d8cf5caf4f0da49815b5527fed7294a88c0b3aa05c73
|
||||
size 18079
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3dd106bc9b54dfc0c4e3e9a5f04d3df52f58f71e4c3cd3d80f53d3f1308f22a1
|
||||
size 16838
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:4979794c700bc8bab1bc767cc2387ce3be28b9d2c0b6da0696b237445ce7df95
|
||||
size 21225
|
||||
|
|
@ -165,6 +165,7 @@
|
|||
"name" : ":features:roomdetails:impl",
|
||||
"includeRegex" : [
|
||||
"screen_room_details_.*",
|
||||
"screen\\.room_details\\..*",
|
||||
"screen_room_member_list_.*",
|
||||
"screen_room_notification_settings_.*",
|
||||
"screen_notification_settings_edit_failed_updating_default_mode",
|
||||
|
|
@ -286,6 +287,12 @@
|
|||
"screen_join_room_.*",
|
||||
"screen\\.join_room\\..*"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name" : ":features:knockrequests:impl",
|
||||
"includeRegex" : [
|
||||
"screen\\.knock_requests_list\\..*"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue