Pinned events : start branching sdk data in the banner.
This commit is contained in:
parent
34fd21f440
commit
f63b59e118
10 changed files with 201 additions and 56 deletions
|
|
@ -102,5 +102,6 @@ dependencies {
|
|||
testImplementation(projects.features.poll.test)
|
||||
testImplementation(projects.features.poll.impl)
|
||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||
testImplementation(projects.libraries.eventformatter.test)
|
||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat
|
|||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerView
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
|
||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||
import io.element.android.features.messages.impl.timeline.TimelineView
|
||||
import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem
|
||||
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet
|
||||
|
|
@ -401,6 +402,9 @@ private fun MessagesViewContent(
|
|||
) {
|
||||
PinnedMessagesBannerView(
|
||||
state = state.pinnedMessagesBannerState,
|
||||
onClick = { pinnedEventId ->
|
||||
//state.timelineState.eventSink(TimelineEvents.FocusOnEvent(pinnedEventId))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ import dagger.Module
|
|||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter
|
||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.di.RoomScope
|
||||
|
||||
@ContributesTo(SessionScope::class)
|
||||
@ContributesTo(RoomScope::class)
|
||||
@Module
|
||||
interface MessagesModule {
|
||||
@Binds
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
|
||||
data class PinnedMessagesBannerItem(
|
||||
val eventId: EventId,
|
||||
val formatted: AnnotatedString,
|
||||
)
|
||||
|
|
@ -17,28 +17,86 @@
|
|||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.text.AnnotatedString
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class PinnedMessagesBannerPresenter @Inject constructor() : Presenter<PinnedMessagesBannerState> {
|
||||
class PinnedMessagesBannerPresenter @Inject constructor(
|
||||
private val room: MatrixRoom,
|
||||
private val pinnedMessagesBannerFormatter: PinnedMessagesBannerFormatter,
|
||||
) : Presenter<PinnedMessagesBannerState> {
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
@Composable
|
||||
override fun present(): PinnedMessagesBannerState {
|
||||
var pinnedMessageCount by remember {
|
||||
mutableIntStateOf(0)
|
||||
var pinnedMessages by remember {
|
||||
mutableStateOf<List<PinnedMessagesBannerItem>>(emptyList())
|
||||
}
|
||||
var currentPinnedMessageIndex by rememberSaveable {
|
||||
mutableIntStateOf(0)
|
||||
}
|
||||
LaunchedEffect(pinnedMessages) {
|
||||
val pinnedMessageCount = pinnedMessages.size
|
||||
if (currentPinnedMessageIndex >= pinnedMessageCount) {
|
||||
currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
|
||||
pinnedEventsTimeline.timelineItems
|
||||
.debounce(300.milliseconds)
|
||||
.map { timelineItems ->
|
||||
timelineItems.mapNotNull { timelineItem ->
|
||||
when (timelineItem) {
|
||||
is MatrixTimelineItem.Event -> {
|
||||
val eventId = timelineItem.eventId ?: return@mapNotNull null
|
||||
val formatted = pinnedMessagesBannerFormatter.format(timelineItem.event)
|
||||
PinnedMessagesBannerItem(
|
||||
eventId = eventId,
|
||||
formatted = if (formatted is AnnotatedString) {
|
||||
formatted
|
||||
} else {
|
||||
AnnotatedString(formatted.toString())
|
||||
},
|
||||
)
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
.onEach { newPinnedMessages ->
|
||||
pinnedMessages = newPinnedMessages
|
||||
}.onCompletion {
|
||||
pinnedEventsTimeline.close()
|
||||
}
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||
when (event) {
|
||||
is PinnedMessagesBannerEvents.MoveToNextPinned -> {
|
||||
if (currentPinnedMessageIndex < pinnedMessageCount - 1) {
|
||||
if (currentPinnedMessageIndex < pinnedMessages.size - 1) {
|
||||
currentPinnedMessageIndex++
|
||||
} else {
|
||||
currentPinnedMessageIndex = 0
|
||||
|
|
@ -48,7 +106,8 @@ class PinnedMessagesBannerPresenter @Inject constructor() : Presenter<PinnedMess
|
|||
}
|
||||
|
||||
return PinnedMessagesBannerState(
|
||||
pinnedMessagesCount = pinnedMessageCount,
|
||||
pinnedMessagesCount = pinnedMessages.size,
|
||||
currentPinnedMessage = pinnedMessages.getOrNull(currentPinnedMessageIndex),
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
eventSink = ::handleEvent
|
||||
)
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ package io.element.android.features.messages.impl.pinned.banner
|
|||
data class PinnedMessagesBannerState(
|
||||
val pinnedMessagesCount: Int,
|
||||
val currentPinnedMessageIndex: Int,
|
||||
val currentPinnedMessage: PinnedMessagesBannerItem?,
|
||||
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||
) {
|
||||
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount
|
||||
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessage != null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@
|
|||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import kotlin.random.Random
|
||||
|
||||
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
|
||||
override val values: Sequence<PinnedMessagesBannerState>
|
||||
|
|
@ -33,9 +36,14 @@ internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<Pinn
|
|||
internal fun aPinnedMessagesBannerState(
|
||||
pinnedMessagesCount: Int = 0,
|
||||
currentPinnedMessageIndex: Int = -1,
|
||||
currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem(
|
||||
eventId = EventId("\$" + Random.nextInt().toString()),
|
||||
formatted = AnnotatedString("This is a pinned message")
|
||||
),
|
||||
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
||||
) = PinnedMessagesBannerState(
|
||||
pinnedMessagesCount = pinnedMessagesCount,
|
||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||
currentPinnedMessage = currentPinnedMessage,
|
||||
eventSink = eventSink
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
|
|
@ -32,8 +33,6 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -42,6 +41,7 @@ import androidx.compose.ui.draw.shadow
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
|
@ -55,39 +55,44 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
|||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerBorder
|
||||
import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndicator
|
||||
import io.element.android.libraries.designsystem.utils.annotatedTextWithBold
|
||||
import io.element.android.libraries.matrix.api.core.EventId
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun PinnedMessagesBannerView(
|
||||
state: PinnedMessagesBannerState,
|
||||
onClick: (EventId) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (state.currentPinnedMessage == null) return
|
||||
|
||||
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
val strokeWidth = 0.5.dp.toPx()
|
||||
val y = size.height - strokeWidth / 2
|
||||
drawLine(
|
||||
borderColor,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
drawLine(
|
||||
borderColor,
|
||||
Offset(0f, 0f),
|
||||
Offset(size.width, 0f),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
.shadow(elevation = 5.dp, spotColor = Color.Transparent)
|
||||
.heightIn(min = 64.dp)
|
||||
.clickable {
|
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
},
|
||||
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
val strokeWidth = 0.5.dp.toPx()
|
||||
val y = size.height - strokeWidth / 2
|
||||
drawLine(
|
||||
borderColor,
|
||||
Offset(0f, y),
|
||||
Offset(size.width, y),
|
||||
strokeWidth
|
||||
)
|
||||
drawLine(
|
||||
borderColor,
|
||||
Offset(0f, 0f),
|
||||
Offset(size.width, 0f),
|
||||
strokeWidth
|
||||
)
|
||||
}
|
||||
.shadow(elevation = 5.dp, spotColor = Color.Transparent)
|
||||
.heightIn(min = 64.dp)
|
||||
.clickable {
|
||||
onClick(state.currentPinnedMessage.eventId)
|
||||
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = spacedBy(10.dp)
|
||||
) {
|
||||
|
|
@ -106,7 +111,7 @@ fun PinnedMessagesBannerView(
|
|||
PinnedMessageItem(
|
||||
index = state.currentPinnedMessageIndex,
|
||||
totalCount = state.pinnedMessagesCount,
|
||||
message = "This is a pinned message",
|
||||
message = state.currentPinnedMessage.formatted,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ })
|
||||
|
|
@ -119,14 +124,12 @@ private fun PinIndicators(
|
|||
pinsCount: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val indicatorHeight by remember {
|
||||
derivedStateOf {
|
||||
when (pinsCount) {
|
||||
0 -> 0
|
||||
1 -> 32
|
||||
2 -> 18
|
||||
else -> 11
|
||||
}
|
||||
val indicatorHeight = remember(pinsCount) {
|
||||
when (pinsCount) {
|
||||
0 -> 0
|
||||
1 -> 32
|
||||
2 -> 18
|
||||
else -> 11
|
||||
}
|
||||
}
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
|
@ -141,20 +144,21 @@ private fun PinIndicators(
|
|||
modifier = modifier,
|
||||
state = lazyListState,
|
||||
verticalArrangement = spacedBy(2.dp),
|
||||
userScrollEnabled = false
|
||||
userScrollEnabled = false,
|
||||
reverseLayout = true
|
||||
) {
|
||||
items(pinsCount) { index ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
.height(indicatorHeight.dp)
|
||||
.background(
|
||||
color = if (index == pinIndex) {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
} else {
|
||||
ElementTheme.colors.pinnedMessageBannerIndicator
|
||||
}
|
||||
)
|
||||
.width(2.dp)
|
||||
.height(indicatorHeight.dp)
|
||||
.background(
|
||||
color = if (index == pinIndex) {
|
||||
ElementTheme.colors.iconAccentPrimary
|
||||
} else {
|
||||
ElementTheme.colors.pinnedMessageBannerIndicator
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +168,13 @@ private fun PinIndicators(
|
|||
private fun PinnedMessageItem(
|
||||
index: Int,
|
||||
totalCount: Int,
|
||||
message: String,
|
||||
message: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount)
|
||||
val fullCountMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, countMessage)
|
||||
Column(modifier = modifier) {
|
||||
if (totalCount > 1) {
|
||||
AnimatedVisibility (totalCount > 1) {
|
||||
Text(
|
||||
text = annotatedTextWithBold(
|
||||
text = fullCountMessage,
|
||||
|
|
@ -179,6 +183,7 @@ private fun PinnedMessageItem(
|
|||
style = ElementTheme.typography.fontBodySmMedium,
|
||||
color = ElementTheme.colors.textActionAccent,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
Text(
|
||||
|
|
@ -196,5 +201,6 @@ private fun PinnedMessageItem(
|
|||
internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview {
|
||||
PinnedMessagesBannerView(
|
||||
state = state,
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,10 @@
|
|||
package io.element.android.features.messages.impl.pinned.banner
|
||||
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||
import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter
|
||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
|
||||
import io.element.android.tests.testutils.test
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
|
|
@ -29,6 +33,7 @@ class PinnedMessagesBannerPresenterTest {
|
|||
val initialState = awaitItem()
|
||||
assertThat(initialState.pinnedMessagesCount).isEqualTo(0)
|
||||
assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0)
|
||||
assertThat(initialState.currentPinnedMessage).isNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -43,7 +48,15 @@ class PinnedMessagesBannerPresenterTest {
|
|||
}
|
||||
}
|
||||
|
||||
private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter {
|
||||
return PinnedMessagesBannerPresenter()
|
||||
private fun createPinnedMessagesBannerPresenter(
|
||||
room: MatrixRoom = FakeMatrixRoom(),
|
||||
formatter: PinnedMessagesBannerFormatter = FakePinnedMessagesBannerFormatter(
|
||||
formatLambda = { event -> "Content ${event.content}" }
|
||||
)
|
||||
): PinnedMessagesBannerPresenter {
|
||||
return PinnedMessagesBannerPresenter(
|
||||
room = room,
|
||||
pinnedMessagesBannerFormatter = formatter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2024 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.eventformatter.test
|
||||
|
||||
import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter
|
||||
import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem
|
||||
|
||||
class FakePinnedMessagesBannerFormatter(
|
||||
val formatLambda: (event: EventTimelineItem) -> CharSequence
|
||||
) : PinnedMessagesBannerFormatter {
|
||||
override fun format(event: EventTimelineItem): CharSequence {
|
||||
return formatLambda(event)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue