Merge pull request #4005 from element-hq/feature/fga/requests_to_join_banner

feat(knock) : Knock Requests Banner UI
This commit is contained in:
ganfra 2024-12-09 11:54:43 +01:00 committed by GitHub
commit 9dac27a165
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 581 additions and 47 deletions

View file

@ -6,7 +6,7 @@
*/
plugins {
id("io.element.android-library")
id("io.element.android-compose-library")
}
android {

View file

@ -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)
}

View file

@ -29,3 +29,17 @@ fun KnockRequest.getAvatarData(size: AvatarSize) = AvatarData(
fun KnockRequest.getBestName(): String {
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,
)

View file

@ -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,
)
}
}

View file

@ -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
}

View file

@ -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,
)
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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,
)

View file

@ -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 = {},
)
}

View file

@ -9,6 +9,7 @@ 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.features.knockrequests.impl.aKnockRequest
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
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(
knockRequests: AsyncData<ImmutableList<KnockRequest>> = AsyncData.Success(persistentListOf()),
currentAction: KnockRequestsCurrentAction = KnockRequestsCurrentAction.None,

View file

@ -383,7 +383,7 @@ private fun KnockRequestsEmptyList(
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()),
iconStyle = BigIcon.Style.Default(CompoundIcons.AskToJoin()),
)
}
}

View file

@ -14,4 +14,12 @@
<string name="screen_knock_requests_list_empty_state_description">"When somebody will ask to join the room, youll 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>
<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>

View file

@ -65,6 +65,7 @@ dependencies {
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.zoomableimage)
implementation(libs.matrix.emojibase.bindings)
implementation(projects.features.knockrequests.api)
testImplementation(libs.test.junit)
testImplementation(libs.coroutines.test)

View file

@ -26,6 +26,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.location.api.Location
import io.element.android.features.location.api.SendLocationEntryPoint
import io.element.android.features.location.api.ShowLocationEntryPoint
@ -95,6 +96,7 @@ class MessagesFlowNode @AssistedInject constructor(
private val mentionSpanTheme: MentionSpanTheme,
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
private val timelineController: TimelineController,
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
backstack = BackStack(
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@ -146,6 +148,9 @@ class MessagesFlowNode @AssistedInject constructor(
@Parcelize
data object PinnedMessagesList : NavTarget
@Parcelize
data object KnockRequestsList : NavTarget
}
private val callbacks = plugins<MessagesEntryPoint.Callback>()
@ -226,6 +231,10 @@ class MessagesFlowNode @AssistedInject constructor(
override fun onViewAllPinnedEvents() {
backstack.push(NavTarget.PinnedMessagesList)
}
override fun onViewKnockRequests() {
backstack.push(NavTarget.KnockRequestsList)
}
}
val inputs = MessagesNode.Inputs(focusedEventId = navTarget.focusedEventId)
createNode<MessagesNode>(buildContext, listOf(callback, inputs))
@ -326,6 +335,9 @@ class MessagesFlowNode @AssistedInject constructor(
NavTarget.Empty -> {
node(buildContext) {}
}
NavTarget.KnockRequestsList -> {
knockRequestsListEntryPoint.createNode(this, buildContext)
}
}
}

View file

@ -28,6 +28,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
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.model.TimelineItemActionPostProcessor
import io.element.android.features.messages.impl.attachments.Attachment
@ -71,6 +72,7 @@ class MessagesNode @AssistedInject constructor(
private val timelineItemPresenterFactories: TimelineItemPresenterFactories,
private val mediaPlayer: MediaPlayer,
private val permalinkParser: PermalinkParser,
private val knockRequestsBannerRenderer: KnockRequestsBannerRenderer
) : Node(buildContext, plugins = plugins), MessagesNavigator {
private val presenter = presenterFactory.create(
navigator = this,
@ -98,6 +100,7 @@ class MessagesNode @AssistedInject constructor(
fun onEditPollClick(eventId: EventId)
fun onJoinCallClick(roomId: RoomId)
fun onViewAllPinnedEvents()
fun onViewKnockRequests()
}
override fun onBuilt() {
@ -206,6 +209,10 @@ class MessagesNode @AssistedInject constructor(
callbacks.forEach { it.onJoinCallClick(room.roomId) }
}
private fun onViewKnockRequestsClick() {
callbacks.forEach { it.onViewKnockRequests() }
}
@Composable
override fun View(modifier: Modifier) {
val activity = LocalContext.current as Activity
@ -231,6 +238,12 @@ class MessagesNode @AssistedInject constructor(
onCreatePollClick = this::onCreatePollClick,
onJoinCallClick = this::onJoinCallClick,
onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick,
knockRequestsBannerView = {
knockRequestsBannerRenderer.View(
modifier = Modifier,
onViewRequestsClick = this::onViewKnockRequestsClick
)
},
modifier = modifier,
)

View file

@ -118,6 +118,7 @@ fun MessagesView(
onViewAllPinnedMessagesClick: () -> Unit,
modifier: Modifier = Modifier,
forceJumpToBottomVisibility: Boolean = false,
knockRequestsBannerView: @Composable () -> Unit,
) {
OnLifecycleEvent { _, event ->
state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event))
@ -195,8 +196,8 @@ fun MessagesView(
MessagesViewContent(
state = state,
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding),
.padding(padding)
.consumeWindowInsets(padding),
onContentClick = ::onContentClick,
onMessageLongClick = ::onMessageLongClick,
onUserDataClick = { hidingKeyboard { onUserDataClick(it) } },
@ -215,6 +216,7 @@ fun MessagesView(
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
onJoinCallClick = onJoinCallClick,
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
knockRequestsBannerView = knockRequestsBannerView,
)
},
snackbarHost = {
@ -284,12 +286,13 @@ private fun MessagesViewContent(
forceJumpToBottomVisibility: Boolean,
onSwipeToReply: (TimelineItem.Event) -> Unit,
modifier: Modifier = Modifier,
knockRequestsBannerView: @Composable () -> Unit,
) {
Box(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding()
.imePadding(),
.fillMaxSize()
.navigationBarsPadding()
.imePadding(),
) {
AttachmentsBottomSheet(
state = state.composerState,
@ -372,6 +375,7 @@ private fun MessagesViewContent(
onViewAllClick = onViewAllPinnedMessagesClick,
)
}
knockRequestsBannerView()
}
},
sheetContent = { subcomposing: Boolean ->
@ -398,13 +402,13 @@ private fun MessagesViewComposerBottomSheetContents(
Column(modifier = Modifier.fillMaxWidth()) {
SuggestionsPickerView(
modifier = Modifier
.heightIn(max = 230.dp)
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available
}
}),
.heightIn(max = 230.dp)
// Consume all scrolling, preventing the bottom sheet from being dragged when interacting with the list of suggestions
.nestedScroll(object : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available
}
}),
roomId = state.roomId,
roomName = state.roomName.dataOrNull(),
roomAvatarData = state.roomAvatar.dataOrNull(),
@ -452,8 +456,8 @@ private fun MessagesViewTopBar(
title = {
val roundedCornerShape = RoundedCornerShape(8.dp)
val titleModifier = Modifier
.clip(roundedCornerShape)
.clickable { onRoomDetailsClick() }
.clip(roundedCornerShape)
.clickable { onRoomDetailsClick() }
if (roomName != null && roomAvatar != null) {
RoomAvatarAndNameRow(
roomName = roomName,
@ -508,9 +512,9 @@ private fun RoomAvatarAndNameRow(
private fun CantSendMessageBanner() {
Row(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary)
.padding(16.dp),
.fillMaxWidth()
.background(MaterialTheme.colorScheme.secondary)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center
) {
@ -539,5 +543,6 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class)
onJoinCallClick = {},
onViewAllPinnedMessagesClick = { },
forceJumpToBottomVisibility = true,
knockRequestsBannerView = {},
)
}

View file

@ -40,5 +40,6 @@ internal fun MessagesViewWithIdentityChangePreview(
onCreatePollClick = {},
onJoinCallClick = {},
onViewAllPinnedMessagesClick = {},
knockRequestsBannerView = {}
)
}

View file

@ -533,6 +533,7 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setMessa
onCreatePollClick = onCreatePollClick,
onJoinCallClick = onJoinCallClick,
onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick,
knockRequestsBannerView = {}
)
}
}

View file

@ -242,7 +242,7 @@ fun RoomDetailsView(
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())),
leadingContent = ListItemContent.Icon(IconSource.Vector(CompoundIcons.AskToJoin())),
trailingContent = if (knockRequestsCount == null || knockRequestsCount == 0) {
null
} else {

View file

@ -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
}

View file

@ -56,4 +56,5 @@ enum class AvatarSize(val dp: Dp) {
Suggestion(32.dp),
KnockRequestItem(52.dp),
KnockRequestBanner(32.dp),
}

View file

@ -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_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">
<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_description">"%1$s Pinned messages"</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_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_roomlist_knock_event_sent_description">"Request to join sent"</string>
<string name="screen_share_location_title">"Share location"</string>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a9595edf8183796a0a0efa838a3c5265e07f70452933e6255d79f536cfca64ac
size 26138
oid sha256:a67bb9ba78ea1d8802ce503e7d6a16be2ff46415df0e435efd7a36f3ee711b0d
size 26453

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:37e24295a404463010f6d36ab94b1ac3c2e193d9122637abbeb0a43abcb427c1
size 25669
oid sha256:be2ae50ce1b0e23fec7bcbbe9ca0f0381af4d0f29f1e7498be837d9d53b75542
size 26003

View file

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

View file

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

View file

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

View file

@ -291,7 +291,9 @@
{
"name" : ":features:knockrequests:impl",
"includeRegex" : [
"screen\\.knock_requests_list\\..*"
"screen\\.knock_requests_list\\..*",
"screen\\.room\\.single_knock_request.*",
"screen\\.room\\.multiple_knock_requests.*"
]
}
]