Merge pull request #4005 from element-hq/feature/fga/requests_to_join_banner
feat(knock) : Knock Requests Banner UI
This commit is contained in:
commit
b603c0e289
42 changed files with 581 additions and 47 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("io.element.android-library")
|
id("io.element.android-compose-library")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
interface KnockRequestsBannerRenderer {
|
||||||
|
@Composable
|
||||||
|
fun View(modifier: Modifier, onViewRequestsClick: () -> Unit)
|
||||||
|
}
|
||||||
|
|
@ -29,3 +29,17 @@ fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
|
||||||
fun KnockRequest.getBestName(): String {
|
fun KnockRequest.getBestName(): String {
|
||||||
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
return displayName?.takeIf { it.isNotEmpty() } ?: userId.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.squareup.anvil.annotations.ContributesBinding
|
||||||
|
import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
|
||||||
|
import io.element.android.libraries.di.RoomScope
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ContributesBinding(RoomScope::class)
|
||||||
|
class DefaultKnockRequestsBannerRenderer @Inject constructor(
|
||||||
|
private val presenter: KnockRequestsBannerPresenter,
|
||||||
|
) : KnockRequestsBannerRenderer {
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier, onViewRequestsClick: () -> Unit) {
|
||||||
|
val state = presenter.present()
|
||||||
|
KnockRequestsBannerView(
|
||||||
|
state = state,
|
||||||
|
onViewRequestsClick = onViewRequestsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
sealed interface KnockRequestsBannerEvents {
|
||||||
|
data object AcceptSingleRequest : KnockRequestsBannerEvents
|
||||||
|
data object Dismiss : KnockRequestsBannerEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class KnockRequestsBannerPresenter @Inject constructor() : Presenter<KnockRequestsBannerState> {
|
||||||
|
@Composable
|
||||||
|
override fun present(): KnockRequestsBannerState {
|
||||||
|
var shouldShowBanner by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
fun handleEvents(event: KnockRequestsBannerEvents) {
|
||||||
|
when (event) {
|
||||||
|
is KnockRequestsBannerEvents.AcceptSingleRequest -> Unit
|
||||||
|
is KnockRequestsBannerEvents.Dismiss -> {
|
||||||
|
shouldShowBanner = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return KnockRequestsBannerState(
|
||||||
|
knockRequests = persistentListOf(),
|
||||||
|
acceptAction = AsyncAction.Uninitialized,
|
||||||
|
canAccept = false,
|
||||||
|
isVisible = shouldShowBanner,
|
||||||
|
eventSink = ::handleEvents,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||||
|
import io.element.android.features.knockrequests.impl.R
|
||||||
|
import io.element.android.features.knockrequests.impl.getBestName
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import io.element.android.libraries.core.extensions.firstIfSingle
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
data class KnockRequestsBannerState(
|
||||||
|
val isVisible: Boolean,
|
||||||
|
val knockRequests: ImmutableList<KnockRequest>,
|
||||||
|
val acceptAction: AsyncAction<Unit>,
|
||||||
|
val canAccept: Boolean,
|
||||||
|
val eventSink: (KnockRequestsBannerEvents) -> Unit,
|
||||||
|
) {
|
||||||
|
val subtitle = knockRequests.firstIfSingle()?.userId?.value
|
||||||
|
val reason = knockRequests.firstIfSingle()?.reason
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun formattedTitle(): String {
|
||||||
|
return when (knockRequests.size) {
|
||||||
|
0 -> ""
|
||||||
|
1 -> stringResource(R.string.screen_room_single_knock_request_title, knockRequests.first().getBestName())
|
||||||
|
else -> {
|
||||||
|
val firstRequest = knockRequests.first()
|
||||||
|
val otherRequestsCount = knockRequests.size - 1
|
||||||
|
pluralStringResource(
|
||||||
|
id = R.plurals.screen_room_multiple_knock_requests_title,
|
||||||
|
count = otherRequestsCount,
|
||||||
|
firstRequest.getBestName(),
|
||||||
|
otherRequestsCount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||||
|
import io.element.android.features.knockrequests.impl.aKnockRequest
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
|
||||||
|
class KnockRequestsBannerStateProvider : PreviewParameterProvider<KnockRequestsBannerState> {
|
||||||
|
override val values: Sequence<KnockRequestsBannerState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aKnockRequestsBannerState(),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
knockRequests = listOf(
|
||||||
|
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."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
knockRequests = listOf(
|
||||||
|
aKnockRequest(),
|
||||||
|
aKnockRequest(displayName = "Alice")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
knockRequests = listOf(
|
||||||
|
aKnockRequest(),
|
||||||
|
aKnockRequest(displayName = "Alice"),
|
||||||
|
aKnockRequest(displayName = "Bob"),
|
||||||
|
aKnockRequest(displayName = "Charlie")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
canAccept = false
|
||||||
|
),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
acceptAction = AsyncAction.Loading
|
||||||
|
),
|
||||||
|
aKnockRequestsBannerState(
|
||||||
|
acceptAction = AsyncAction.Failure(Throwable("Failed to accept knock"))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aKnockRequestsBannerState(
|
||||||
|
knockRequests: List<KnockRequest> = listOf(aKnockRequest()),
|
||||||
|
acceptAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
|
canAccept: Boolean = true,
|
||||||
|
isVisible: Boolean = true,
|
||||||
|
eventSink: (KnockRequestsBannerEvents) -> Unit = {}
|
||||||
|
) = KnockRequestsBannerState(
|
||||||
|
knockRequests = knockRequests.toImmutableList(),
|
||||||
|
acceptAction = acceptAction,
|
||||||
|
canAccept = canAccept,
|
||||||
|
isVisible = isVisible,
|
||||||
|
eventSink = eventSink,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,217 @@
|
||||||
|
/*
|
||||||
|
* 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.banner
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
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.libraries.designsystem.components.avatar.Avatar
|
||||||
|
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Button
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.ButtonSize
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.OutlinedButton
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
|
private const val MAX_AVATAR_COUNT = 3
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KnockRequestsBannerView(
|
||||||
|
state: KnockRequestsBannerState,
|
||||||
|
onViewRequestsClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.isVisible,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.small,
|
||||||
|
color = ElementTheme.colors.bgCanvasDefaultLevel1,
|
||||||
|
shadowElevation = 24.dp,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
) {
|
||||||
|
KnockRequestsBannerContent(
|
||||||
|
state = state,
|
||||||
|
onViewRequestsClick = onViewRequestsClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KnockRequestsBannerContent(
|
||||||
|
state: KnockRequestsBannerState,
|
||||||
|
onViewRequestsClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
fun onDismissClick() {
|
||||||
|
state.eventSink(KnockRequestsBannerEvents.Dismiss)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onAcceptClick() {
|
||||||
|
state.eventSink(KnockRequestsBannerEvents.AcceptSingleRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
KnockRequestAvatarView(state.knockRequests)
|
||||||
|
Spacer(modifier = Modifier.width(10.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = state.formattedTitle(),
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
if (state.subtitle != null) {
|
||||||
|
Text(
|
||||||
|
text = state.subtitle,
|
||||||
|
style = ElementTheme.typography.fontBodySmRegular,
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
textAlign = TextAlign.Start,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.clickable(onClick = ::onDismissClick),
|
||||||
|
imageVector = CompoundIcons.Close(),
|
||||||
|
contentDescription = stringResource(CommonStrings.action_close)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.reason != null) {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = state.reason,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||||
|
if (state.knockRequests.size > 1) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_room_multiple_knock_requests_view_all_button_title),
|
||||||
|
onClick = onViewRequestsClick,
|
||||||
|
size = ButtonSize.MediumLowPadding,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
OutlinedButton(
|
||||||
|
text = stringResource(R.string.screen_room_single_knock_request_view_button_title),
|
||||||
|
onClick = onViewRequestsClick,
|
||||||
|
size = ButtonSize.MediumLowPadding,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
if (state.canAccept) {
|
||||||
|
Button(
|
||||||
|
text = stringResource(R.string.screen_room_single_knock_request_accept_button_title),
|
||||||
|
onClick = ::onAcceptClick,
|
||||||
|
size = ButtonSize.MediumLowPadding,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KnockRequestAvatarView(
|
||||||
|
knockRequests: ImmutableList<KnockRequest>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier) {
|
||||||
|
when (knockRequests.size) {
|
||||||
|
0 -> Unit
|
||||||
|
1 -> Avatar(knockRequests.first().getAvatarData(AvatarSize.KnockRequestBanner))
|
||||||
|
else -> KnockRequestAvatarListView(knockRequests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun KnockRequestAvatarListView(
|
||||||
|
knockRequests: ImmutableList<KnockRequest>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val avatarSize = AvatarSize.KnockRequestBanner.dp
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(-avatarSize / 2),
|
||||||
|
) {
|
||||||
|
knockRequests
|
||||||
|
.take(MAX_AVATAR_COUNT)
|
||||||
|
.forEachIndexed { index, knockRequest ->
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(size = avatarSize)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color = ElementTheme.colors.bgCanvasDefaultLevel1)
|
||||||
|
.zIndex(-index.toFloat()),
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@PreviewsDayNight
|
||||||
|
internal fun KnockRequestsBannerViewPreview(@PreviewParameter(KnockRequestsBannerStateProvider::class) state: KnockRequestsBannerState) = ElementPreview {
|
||||||
|
KnockRequestsBannerView(
|
||||||
|
state = state,
|
||||||
|
onViewRequestsClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.knockrequests.impl.list
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.features.knockrequests.impl.KnockRequest
|
import io.element.android.features.knockrequests.impl.KnockRequest
|
||||||
|
import io.element.android.features.knockrequests.impl.aKnockRequest
|
||||||
import io.element.android.libraries.architecture.AsyncAction
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
|
|
@ -101,20 +102,6 @@ open class KnockRequestsListStateProvider : PreviewParameterProvider<KnockReques
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
fun aKnockRequestsListState(
|
||||||
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
|
||||||
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,
|
||||||
|
|
|
||||||
|
|
@ -383,7 +383,7 @@ private fun KnockRequestsEmptyList(
|
||||||
IconTitleSubtitleMolecule(
|
IconTitleSubtitleMolecule(
|
||||||
title = stringResource(R.string.screen_knock_requests_list_empty_state_title),
|
title = stringResource(R.string.screen_knock_requests_list_empty_state_title),
|
||||||
subTitle = stringResource(R.string.screen_knock_requests_list_empty_state_description),
|
subTitle = stringResource(R.string.screen_knock_requests_list_empty_state_description),
|
||||||
iconStyle = BigIcon.Style.Default(CompoundIcons.Pin()),
|
iconStyle = BigIcon.Style.Default(CompoundIcons.AskToJoin()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,4 +14,12 @@
|
||||||
<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_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_empty_state_title">"No pending request to join"</string>
|
||||||
<string name="screen_knock_requests_list_title">"Requests to join"</string>
|
<string name="screen_knock_requests_list_title">"Requests to join"</string>
|
||||||
|
<plurals name="screen_room_multiple_knock_requests_title">
|
||||||
|
<item quantity="one">"%1$s +%2$d other want to join this room"</item>
|
||||||
|
<item quantity="other">"%1$s +%2$d others want to join this room"</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="screen_room_multiple_knock_requests_view_all_button_title">"View all"</string>
|
||||||
|
<string name="screen_room_single_knock_request_accept_button_title">"Accept"</string>
|
||||||
|
<string name="screen_room_single_knock_request_title">"%1$s wants to join this room"</string>
|
||||||
|
<string name="screen_room_single_knock_request_view_button_title">"View"</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ dependencies {
|
||||||
implementation(libs.vanniktech.blurhash)
|
implementation(libs.vanniktech.blurhash)
|
||||||
implementation(libs.telephoto.zoomableimage)
|
implementation(libs.telephoto.zoomableimage)
|
||||||
implementation(libs.matrix.emojibase.bindings)
|
implementation(libs.matrix.emojibase.bindings)
|
||||||
|
implementation(projects.features.knockrequests.api)
|
||||||
|
|
||||||
testImplementation(libs.test.junit)
|
testImplementation(libs.test.junit)
|
||||||
testImplementation(libs.coroutines.test)
|
testImplementation(libs.coroutines.test)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import im.vector.app.features.analytics.plan.Interaction
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.call.api.CallType
|
import io.element.android.features.call.api.CallType
|
||||||
import io.element.android.features.call.api.ElementCallEntryPoint
|
import io.element.android.features.call.api.ElementCallEntryPoint
|
||||||
|
import io.element.android.features.knockrequests.api.list.KnockRequestsListEntryPoint
|
||||||
import io.element.android.features.location.api.Location
|
import io.element.android.features.location.api.Location
|
||||||
import io.element.android.features.location.api.SendLocationEntryPoint
|
import io.element.android.features.location.api.SendLocationEntryPoint
|
||||||
import io.element.android.features.location.api.ShowLocationEntryPoint
|
import io.element.android.features.location.api.ShowLocationEntryPoint
|
||||||
|
|
@ -95,6 +96,7 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
private val mentionSpanTheme: MentionSpanTheme,
|
private val mentionSpanTheme: MentionSpanTheme,
|
||||||
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
|
||||||
private val timelineController: TimelineController,
|
private val timelineController: TimelineController,
|
||||||
|
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
|
||||||
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
|
||||||
backstack = BackStack(
|
backstack = BackStack(
|
||||||
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
|
||||||
|
|
@ -146,6 +148,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data object PinnedMessagesList : NavTarget
|
data object PinnedMessagesList : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data object KnockRequestsList : NavTarget
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callbacks = plugins<MessagesEntryPoint.Callback>()
|
private val callbacks = plugins<MessagesEntryPoint.Callback>()
|
||||||
|
|
@ -226,6 +231,10 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
override fun onViewAllPinnedEvents() {
|
override fun onViewAllPinnedEvents() {
|
||||||
backstack.push(NavTarget.PinnedMessagesList)
|
backstack.push(NavTarget.PinnedMessagesList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onViewKnockRequests() {
|
||||||
|
backstack.push(NavTarget.KnockRequestsList)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
|
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
|
||||||
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
|
||||||
|
|
@ -326,6 +335,9 @@ class MessagesFlowNode @AssistedInject constructor(
|
||||||
NavTarget.Empty -> {
|
NavTarget.Empty -> {
|
||||||
node(buildContext) {}
|
node(buildContext) {}
|
||||||
}
|
}
|
||||||
|
NavTarget.KnockRequestsList -> {
|
||||||
|
knockRequestsListEntryPoint.createNode(this, buildContext)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.knockrequests.api.banner.KnockRequestsBannerRenderer
|
||||||
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
|
||||||
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
import io.element.android.features.messages.impl.actionlist.model.TimelineItemActionPostProcessor
|
||||||
import io.element.android.features.messages.impl.attachments.Attachment
|
import io.element.android.features.messages.impl.attachments.Attachment
|
||||||
|
|
@ -71,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
|
||||||
private val mediaPlayer: MediaPlayer,
|
private val mediaPlayer: MediaPlayer,
|
||||||
private val permalinkParser: PermalinkParser,
|
private val permalinkParser: PermalinkParser,
|
||||||
|
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer
|
||||||
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
) : Node(buildContext, plugins = plugins), MessagesNavigator {
|
||||||
private val presenter = presenterFactory.create(
|
private val presenter = presenterFactory.create(
|
||||||
navigator = this,
|
navigator = this,
|
||||||
|
|
@ -98,6 +100,7 @@ class MessagesNode @AssistedInject constructor(
|
||||||
fun onEditPollClick(eventId: EventId)
|
fun onEditPollClick(eventId: EventId)
|
||||||
fun onJoinCallClick(roomId: RoomId)
|
fun onJoinCallClick(roomId: RoomId)
|
||||||
fun onViewAllPinnedEvents()
|
fun onViewAllPinnedEvents()
|
||||||
|
fun onViewKnockRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBuilt() {
|
override fun onBuilt() {
|
||||||
|
|
@ -206,6 +209,10 @@ class MessagesNode @AssistedInject constructor(
|
||||||
callbacks.forEach { it.onJoinCallClick(room.roomId) }
|
callbacks.forEach { it.onJoinCallClick(room.roomId) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onViewKnockRequestsClick() {
|
||||||
|
callbacks.forEach { it.onViewKnockRequests() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val activity = LocalContext.current as Activity
|
val activity = LocalContext.current as Activity
|
||||||
|
|
@ -231,6 +238,12 @@ class MessagesNode @AssistedInject constructor(
|
||||||
onCreatePollClick = this::onCreatePollClick,
|
onCreatePollClick = this::onCreatePollClick,
|
||||||
onJoinCallClick = this::onJoinCallClick,
|
onJoinCallClick = this::onJoinCallClick,
|
||||||
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
|
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
|
||||||
|
knockRequestsBannerView = {
|
||||||
|
knockRequestsBannerRenderer.View(
|
||||||
|
modifier = Modifier,
|
||||||
|
onViewRequestsClick = this::onViewKnockRequestsClick
|
||||||
|
)
|
||||||
|
},
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ fun MessagesView(
|
||||||
onViewAllPinnedMessagesClick: () -> Unit,
|
onViewAllPinnedMessagesClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
forceJumpToBottomVisibility: Boolean = false,
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
|
knockRequestsBannerView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
OnLifecycleEvent { _, event ->
|
OnLifecycleEvent { _, event ->
|
||||||
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
|
||||||
|
|
@ -195,8 +196,8 @@ fun MessagesView(
|
||||||
MessagesViewContent(
|
MessagesViewContent(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.consumeWindowInsets(padding),
|
.consumeWindowInsets(padding),
|
||||||
onContentClick = ::onContentClick,
|
onContentClick = ::onContentClick,
|
||||||
onMessageLongClick = ::onMessageLongClick,
|
onMessageLongClick = ::onMessageLongClick,
|
||||||
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
|
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
|
||||||
|
|
@ -215,6 +216,7 @@ fun MessagesView(
|
||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
||||||
|
knockRequestsBannerView = knockRequestsBannerView,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
snackbarHost = {
|
snackbarHost = {
|
||||||
|
|
@ -284,12 +286,13 @@ private fun MessagesViewContent(
|
||||||
forceJumpToBottomVisibility: Boolean,
|
forceJumpToBottomVisibility: Boolean,
|
||||||
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
onSwipeToReply: (TimelineItem.Event) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
knockRequestsBannerView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.navigationBarsPadding()
|
.navigationBarsPadding()
|
||||||
.imePadding(),
|
.imePadding(),
|
||||||
) {
|
) {
|
||||||
AttachmentsBottomSheet(
|
AttachmentsBottomSheet(
|
||||||
state = state.composerState,
|
state = state.composerState,
|
||||||
|
|
@ -372,6 +375,7 @@ private fun MessagesViewContent(
|
||||||
onViewAllClick = onViewAllPinnedMessagesClick,
|
onViewAllClick = onViewAllPinnedMessagesClick,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
knockRequestsBannerView()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sheetContent = { subcomposing: Boolean ->
|
sheetContent = { subcomposing: Boolean ->
|
||||||
|
|
@ -398,13 +402,13 @@ private fun MessagesViewComposerBottomSheetContents(
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
SuggestionsPickerView(
|
SuggestionsPickerView(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.heightIn(max = 230.dp)
|
.heightIn(max = 230.dp)
|
||||||
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
|
||||||
.nestedScroll(object : NestedScrollConnection {
|
.nestedScroll(object : NestedScrollConnection {
|
||||||
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
|
||||||
return available
|
return available
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
roomId = state.roomId,
|
roomId = state.roomId,
|
||||||
roomName = state.roomName.dataOrNull(),
|
roomName = state.roomName.dataOrNull(),
|
||||||
roomAvatarData = state.roomAvatar.dataOrNull(),
|
roomAvatarData = state.roomAvatar.dataOrNull(),
|
||||||
|
|
@ -452,8 +456,8 @@ private fun MessagesViewTopBar(
|
||||||
title = {
|
title = {
|
||||||
val roundedCornerShape = RoundedCornerShape(8.dp)
|
val roundedCornerShape = RoundedCornerShape(8.dp)
|
||||||
val titleModifier = Modifier
|
val titleModifier = Modifier
|
||||||
.clip(roundedCornerShape)
|
.clip(roundedCornerShape)
|
||||||
.clickable { onRoomDetailsClick() }
|
.clickable { onRoomDetailsClick() }
|
||||||
if (roomName != null && roomAvatar != null) {
|
if (roomName != null && roomAvatar != null) {
|
||||||
RoomAvatarAndNameRow(
|
RoomAvatarAndNameRow(
|
||||||
roomName = roomName,
|
roomName = roomName,
|
||||||
|
|
@ -508,9 +512,9 @@ private fun RoomAvatarAndNameRow(
|
||||||
private fun CantSendMessageBanner() {
|
private fun CantSendMessageBanner() {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(MaterialTheme.colorScheme.secondary)
|
.background(MaterialTheme.colorScheme.secondary)
|
||||||
.padding(16.dp),
|
.padding(16.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.Center
|
horizontalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
|
@ -539,5 +543,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
onViewAllPinnedMessagesClick = { },
|
onViewAllPinnedMessagesClick = { },
|
||||||
forceJumpToBottomVisibility = true,
|
forceJumpToBottomVisibility = true,
|
||||||
|
knockRequestsBannerView = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,5 +40,6 @@ internal fun MessagesViewWithIdentityChangePreview(
|
||||||
onCreatePollClick = {},
|
onCreatePollClick = {},
|
||||||
onJoinCallClick = {},
|
onJoinCallClick = {},
|
||||||
onViewAllPinnedMessagesClick = {},
|
onViewAllPinnedMessagesClick = {},
|
||||||
|
knockRequestsBannerView = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -533,6 +533,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
|
||||||
onCreatePollClick = onCreatePollClick,
|
onCreatePollClick = onCreatePollClick,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
|
||||||
|
knockRequestsBannerView = {}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -242,7 +242,7 @@ fun RoomDetailsView(
|
||||||
private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: () -> Unit) {
|
private fun KnockRequestsItem(knockRequestsCount: Int?, onKnockRequestsClick: () -> Unit) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.screen_room_details_requests_to_join_title)) },
|
headlineContent = { Text(stringResource(R.string.screen_room_details_requests_to_join_title)) },
|
||||||
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.Notifications())),
|
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.AskToJoin())),
|
||||||
trailingContent = if (knockRequestsCount == null || knockRequestsCount == 0) {
|
trailingContent = if (knockRequestsCount == null || knockRequestsCount == 0) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
/*
|
||||||
|
* 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.libraries.core.extensions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the first element if the list contains exactly one element, otherwise returns null.
|
||||||
|
*/
|
||||||
|
inline fun <reified T> List<T>.firstIfSingle(): T? {
|
||||||
|
return if (size == 1) first() else null
|
||||||
|
}
|
||||||
|
|
@ -56,4 +56,5 @@ enum class AvatarSize(val dp: Dp) {
|
||||||
Suggestion(32.dp),
|
Suggestion(32.dp),
|
||||||
|
|
||||||
KnockRequestItem(52.dp),
|
KnockRequestItem(52.dp),
|
||||||
|
KnockRequestBanner(32.dp),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -322,18 +322,10 @@ Reason: %1$s."</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_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_error_failed_processing_media">"Failed processing media to upload, please try again."</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>
|
<string name="screen_room_error_failed_retrieving_user_details">"Could not retrieve user details"</string>
|
||||||
<plurals name="screen_room_multiple_knock_requests_title">
|
|
||||||
<item quantity="one">"%1$s +%2$d other want to join this room"</item>
|
|
||||||
<item quantity="other">"%1$s +%2$d others want to join this room"</item>
|
|
||||||
</plurals>
|
|
||||||
<string name="screen_room_multiple_knock_requests_view_all_button_title">"View all"</string>
|
|
||||||
<string name="screen_room_pinned_banner_indicator">"%1$s of %2$s"</string>
|
<string name="screen_room_pinned_banner_indicator">"%1$s of %2$s"</string>
|
||||||
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pinned messages"</string>
|
<string name="screen_room_pinned_banner_indicator_description">"%1$s Pinned messages"</string>
|
||||||
<string name="screen_room_pinned_banner_loading_description">"Loading message…"</string>
|
<string name="screen_room_pinned_banner_loading_description">"Loading message…"</string>
|
||||||
<string name="screen_room_pinned_banner_view_all_button_title">"View All"</string>
|
<string name="screen_room_pinned_banner_view_all_button_title">"View All"</string>
|
||||||
<string name="screen_room_single_knock_request_accept_button_title">"Accept"</string>
|
|
||||||
<string name="screen_room_single_knock_request_title">"%1$s wants to join this room"</string>
|
|
||||||
<string name="screen_room_single_knock_request_view_button_title">"View"</string>
|
|
||||||
<string name="screen_room_title">"Chat"</string>
|
<string name="screen_room_title">"Chat"</string>
|
||||||
<string name="screen_roomlist_knock_event_sent_description">"Request to join sent"</string>
|
<string name="screen_roomlist_knock_event_sent_description">"Request to join sent"</string>
|
||||||
<string name="screen_share_location_title">"Share location"</string>
|
<string name="screen_share_location_title">"Share location"</string>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d20f97f6422fb2eaaccd13ab12b3e27e589eaafe032a65696f3c862ccae4b743
|
||||||
|
size 29335
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:e0f508d82473c7b1dc9da20d25808843c3e1bfcac632ac2bf33151cc7626bf35
|
||||||
|
size 34821
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5262c97044361ab44066a8876b8417853c8ef5c1449390242c0248c06a0fc568
|
||||||
|
size 17855
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0f247767c5f965cf75f1c7556f49bd5e3c7207b2effd9cd3d19a913581556842
|
||||||
|
size 18744
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:78f259c410ad179c3f93b18fc7c10829a355e5a0f56b0dd4c1f4b4efc72910f0
|
||||||
|
size 27309
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d20f97f6422fb2eaaccd13ab12b3e27e589eaafe032a65696f3c862ccae4b743
|
||||||
|
size 29335
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:d20f97f6422fb2eaaccd13ab12b3e27e589eaafe032a65696f3c862ccae4b743
|
||||||
|
size 29335
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a965026cd6257bd691bdf821dcb29c99276ab5aebff2060610acc287cebf4c35
|
||||||
|
size 27357
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:77bc7bd3f4dab9e05ce7f68ddc50d06da03d414c33ce8e361bb83ccec38ad3c8
|
||||||
|
size 32231
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:cff0d41eb0ab572946867cb9ea57fcab1fb72b155273fc68b12855c100cdb0c0
|
||||||
|
size 15974
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:8fcbb3f101b864cd0a5eeade5d43a76e42573705a90604706be696a810e7096c
|
||||||
|
size 17021
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:f89ebacc5f45844b3606a214a4c0df8ca150003de50bd7a04680d85ddea2ee15
|
||||||
|
size 25224
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a965026cd6257bd691bdf821dcb29c99276ab5aebff2060610acc287cebf4c35
|
||||||
|
size 27357
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a965026cd6257bd691bdf821dcb29c99276ab5aebff2060610acc287cebf4c35
|
||||||
|
size 27357
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a9595edf8183796a0a0efa838a3c5265e07f70452933e6255d79f536cfca64ac
|
oid sha256:a67bb9ba78ea1d8802ce503e7d6a16be2ff46415df0e435efd7a36f3ee711b0d
|
||||||
size 26138
|
size 26453
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:37e24295a404463010f6d36ab94b1ac3c2e193d9122637abbeb0a43abcb427c1
|
oid sha256:be2ae50ce1b0e23fec7bcbbe9ca0f0381af4d0f29f1e7498be837d9d53b75542
|
||||||
size 25669
|
size 26003
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5abc97134f8a5f0d037367c9278d8f007543463e13c8e043241f292949681ff6
|
||||||
|
size 17728
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:972653df5c62859c175a62d7809193afd0cb68832e3567760082f1b164e3424b
|
||||||
|
size 16990
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:491cb82462df122b0e8d21c5b70e8db8ac19c28aaee1289db6f4774e7d31d53f
|
||||||
|
size 19556
|
||||||
|
|
@ -291,7 +291,9 @@
|
||||||
{
|
{
|
||||||
"name" : ":features:knockrequests:impl",
|
"name" : ":features:knockrequests:impl",
|
||||||
"includeRegex" : [
|
"includeRegex" : [
|
||||||
"screen\\.knock_requests_list\\..*"
|
"screen\\.knock_requests_list\\..*",
|
||||||
|
"screen\\.room\\.single_knock_request.*",
|
||||||
|
"screen\\.room\\.multiple_knock_requests.*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue