Pinned events : handle loading/error on banner
This commit is contained in:
parent
d29ffc653c
commit
4771b25fe3
8 changed files with 238 additions and 97 deletions
|
|
@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineState
|
import io.element.android.features.messages.impl.timeline.TimelineState
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
import io.element.android.features.messages.impl.timeline.aTimelineItemList
|
||||||
import io.element.android.features.messages.impl.timeline.aTimelineState
|
import io.element.android.features.messages.impl.timeline.aTimelineState
|
||||||
|
|
@ -90,8 +90,8 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
|
||||||
callState = RoomCallState.DISABLED,
|
callState = RoomCallState.DISABLED,
|
||||||
),
|
),
|
||||||
aMessagesState(
|
aMessagesState(
|
||||||
pinnedMessagesBannerState = aPinnedMessagesBannerState(
|
pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(
|
||||||
pinnedMessagesCount = 4,
|
knownPinnedMessagesCount = 4,
|
||||||
currentPinnedMessageIndex = 0,
|
currentPinnedMessageIndex = 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -121,7 +121,7 @@ fun aMessagesState(
|
||||||
showReinvitePrompt: Boolean = false,
|
showReinvitePrompt: Boolean = false,
|
||||||
enableVoiceMessages: Boolean = true,
|
enableVoiceMessages: Boolean = true,
|
||||||
callState: RoomCallState = RoomCallState.ENABLED,
|
callState: RoomCallState = RoomCallState.ENABLED,
|
||||||
pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(),
|
pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(),
|
||||||
eventSink: (MessagesEvents) -> Unit = {},
|
eventSink: (MessagesEvents) -> Unit = {},
|
||||||
) = MessagesState(
|
) = MessagesState(
|
||||||
roomId = RoomId("!id:domain"),
|
roomId = RoomId("!id:domain"),
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott
|
||||||
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
import io.element.android.features.messages.impl.messagecomposer.AttachmentsState
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
|
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.messagecomposer.MessageComposerView
|
||||||
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView
|
||||||
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
|
import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
import io.element.android.features.messages.impl.timeline.TimelineEvents
|
||||||
|
|
@ -400,7 +401,7 @@ private fun MessagesViewContent(
|
||||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = state.pinnedMessagesBannerState.displayBanner && scrollBehavior.isVisible,
|
visible = state.pinnedMessagesBannerState != PinnedMessagesBannerState.Hidden && scrollBehavior.isVisible,
|
||||||
enter = expandVertically(),
|
enter = expandVertically(),
|
||||||
exit = shrinkVertically(),
|
exit = shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
|
|
|
||||||
|
|
@ -26,10 +26,14 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.features.networkmonitor.api.NetworkMonitor
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
import io.element.android.libraries.matrix.api.room.MatrixRoom
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
|
@ -43,29 +47,34 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
||||||
private val room: MatrixRoom,
|
private val room: MatrixRoom,
|
||||||
private val itemFactory: PinnedMessagesBannerItemFactory,
|
private val itemFactory: PinnedMessagesBannerItemFactory,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
|
private val networkMonitor: NetworkMonitor,
|
||||||
) : Presenter<PinnedMessagesBannerState> {
|
) : Presenter<PinnedMessagesBannerState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): PinnedMessagesBannerState {
|
override fun present(): PinnedMessagesBannerState {
|
||||||
|
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
|
||||||
|
var timelineFailed by rememberSaveable { mutableStateOf(false) }
|
||||||
var pinnedItems by remember {
|
var pinnedItems by remember {
|
||||||
mutableStateOf<List<PinnedMessagesBannerItem>>(emptyList())
|
mutableStateOf<ImmutableList<PinnedMessagesBannerItem>>(persistentListOf())
|
||||||
}
|
}
|
||||||
|
val knownPinnedMessagesCount by remember {
|
||||||
|
room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size }
|
||||||
|
}.collectAsState(initial = null)
|
||||||
|
|
||||||
fun onItemsChange(newItems: List<PinnedMessagesBannerItem>) {
|
var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) }
|
||||||
pinnedItems = newItems
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentPinnedMessageIndex by rememberSaveable {
|
PinnedMessagesBannerItemsEffect(
|
||||||
mutableIntStateOf(0)
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
}
|
onItemsChange = { newItems ->
|
||||||
|
val pinnedMessageCount = newItems.size
|
||||||
LaunchedEffect(pinnedItems) {
|
if (currentPinnedMessageIndex >= pinnedMessageCount) {
|
||||||
val pinnedMessageCount = pinnedItems.size
|
currentPinnedMessageIndex = 0
|
||||||
if (currentPinnedMessageIndex >= pinnedMessageCount) {
|
}
|
||||||
currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0)
|
pinnedItems = newItems
|
||||||
|
},
|
||||||
|
onTimelineFail = { hasTimelineFailed ->
|
||||||
|
timelineFailed = hasTimelineFailed
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
PinnedMessagesBannerItemsEffect(::onItemsChange)
|
|
||||||
|
|
||||||
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
fun handleEvent(event: PinnedMessagesBannerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
|
|
@ -79,32 +88,68 @@ class PinnedMessagesBannerPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return PinnedMessagesBannerState(
|
return pinnedMessagesBannerState(
|
||||||
pinnedMessagesCount = pinnedItems.size,
|
isFeatureEnabled = isFeatureEnabled,
|
||||||
currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex),
|
hasTimelineFailed = timelineFailed,
|
||||||
|
realPinnedMessagesCount = knownPinnedMessagesCount,
|
||||||
|
pinnedItems = pinnedItems,
|
||||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
eventSink = ::handleEvent
|
eventSink = ::handleEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun pinnedMessagesBannerState(
|
||||||
|
isFeatureEnabled: Boolean,
|
||||||
|
hasTimelineFailed: Boolean,
|
||||||
|
realPinnedMessagesCount: Int?,
|
||||||
|
pinnedItems: ImmutableList<PinnedMessagesBannerItem>,
|
||||||
|
currentPinnedMessageIndex: Int,
|
||||||
|
eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
|
): PinnedMessagesBannerState {
|
||||||
|
val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex)
|
||||||
|
return when {
|
||||||
|
!isFeatureEnabled -> PinnedMessagesBannerState.Hidden
|
||||||
|
hasTimelineFailed -> PinnedMessagesBannerState.Hidden
|
||||||
|
realPinnedMessagesCount == null || realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden
|
||||||
|
currentPinnedMessage == null -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount)
|
||||||
|
else -> {
|
||||||
|
PinnedMessagesBannerState.Loaded(
|
||||||
|
currentPinnedMessage = currentPinnedMessage,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
knownPinnedMessagesCount = realPinnedMessagesCount,
|
||||||
|
eventSink = eventSink
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(FlowPreview::class)
|
@OptIn(FlowPreview::class)
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinnedMessagesBannerItemsEffect(
|
private fun PinnedMessagesBannerItemsEffect(
|
||||||
onItemsChange: (List<PinnedMessagesBannerItem>) -> Unit,
|
isFeatureEnabled: Boolean,
|
||||||
|
onItemsChange: (ImmutableList<PinnedMessagesBannerItem>) -> Unit,
|
||||||
|
onTimelineFail: (Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false)
|
|
||||||
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
val updatedOnItemsChange by rememberUpdatedState(onItemsChange)
|
||||||
|
val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail)
|
||||||
|
val networkStatus by networkMonitor.connectivity.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(isFeatureEnabled) {
|
LaunchedEffect(isFeatureEnabled, networkStatus) {
|
||||||
if (!isFeatureEnabled) return@LaunchedEffect
|
if (!isFeatureEnabled) return@LaunchedEffect
|
||||||
|
|
||||||
val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect
|
val pinnedEventsTimeline = room.pinnedEventsTimeline()
|
||||||
|
.onFailure { updatedOnTimelineFail(true) }
|
||||||
|
.onSuccess { updatedOnTimelineFail(false) }
|
||||||
|
.getOrNull()
|
||||||
|
?: return@LaunchedEffect
|
||||||
|
|
||||||
pinnedEventsTimeline.timelineItems
|
pinnedEventsTimeline.timelineItems
|
||||||
.debounce(300.milliseconds)
|
.debounce(300.milliseconds)
|
||||||
.map { timelineItems ->
|
.map { timelineItems ->
|
||||||
timelineItems.mapNotNull { timelineItem ->
|
timelineItems.mapNotNull { timelineItem ->
|
||||||
itemFactory.create(timelineItem)
|
itemFactory.create(timelineItem)
|
||||||
}
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
.onEach { newItems ->
|
.onEach { newItems ->
|
||||||
updatedOnItemsChange(newItems)
|
updatedOnItemsChange(newItems)
|
||||||
|
|
|
||||||
|
|
@ -16,11 +16,40 @@
|
||||||
|
|
||||||
package io.element.android.features.messages.impl.pinned.banner
|
package io.element.android.features.messages.impl.pinned.banner
|
||||||
|
|
||||||
data class PinnedMessagesBannerState(
|
import androidx.compose.runtime.Composable
|
||||||
val pinnedMessagesCount: Int,
|
import androidx.compose.runtime.Immutable
|
||||||
val currentPinnedMessageIndex: Int,
|
import androidx.compose.ui.res.stringResource
|
||||||
val currentPinnedMessage: PinnedMessagesBannerItem?,
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
import io.element.android.libraries.designsystem.text.toAnnotatedString
|
||||||
) {
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessage != null
|
|
||||||
|
@Immutable
|
||||||
|
sealed interface PinnedMessagesBannerState {
|
||||||
|
data object Hidden : PinnedMessagesBannerState
|
||||||
|
data class Loading(val realPinnedMessagesCount: Int) : PinnedMessagesBannerState
|
||||||
|
data class Loaded(
|
||||||
|
val currentPinnedMessage: PinnedMessagesBannerItem,
|
||||||
|
val currentPinnedMessageIndex: Int,
|
||||||
|
val knownPinnedMessagesCount: Int,
|
||||||
|
val eventSink: (PinnedMessagesBannerEvents) -> Unit
|
||||||
|
) : PinnedMessagesBannerState
|
||||||
|
|
||||||
|
fun pinnedMessagesCount() = when (this) {
|
||||||
|
is Hidden -> 0
|
||||||
|
is Loading -> realPinnedMessagesCount
|
||||||
|
is Loaded -> knownPinnedMessagesCount
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentPinnedMessageIndex() = when (this) {
|
||||||
|
is Hidden -> 0
|
||||||
|
is Loading -> 0
|
||||||
|
is Loaded -> currentPinnedMessageIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun formattedMessage() = when (this) {
|
||||||
|
is Hidden -> AnnotatedString("")
|
||||||
|
is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString()
|
||||||
|
is Loaded -> currentPinnedMessage.formatted
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,26 +24,38 @@ import kotlin.random.Random
|
||||||
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
|
internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider<PinnedMessagesBannerState> {
|
||||||
override val values: Sequence<PinnedMessagesBannerState>
|
override val values: Sequence<PinnedMessagesBannerState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 1, currentPinnedMessageIndex = 0),
|
aHiddenPinnedMessagesBannerState(),
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 2, currentPinnedMessageIndex = 0),
|
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 1),
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 0),
|
aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 4),
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 1),
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 1, currentPinnedMessageIndex = 0),
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 2),
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 2, currentPinnedMessageIndex = 0),
|
||||||
aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 3),
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 3, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 1),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 2),
|
||||||
|
aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 3),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun aPinnedMessagesBannerState(
|
internal fun aHiddenPinnedMessagesBannerState() = PinnedMessagesBannerState.Hidden
|
||||||
pinnedMessagesCount: Int = 0,
|
|
||||||
currentPinnedMessageIndex: Int = -1,
|
internal fun aLoadingPinnedMessagesBannerState(
|
||||||
|
knownPinnedMessagesCount: Int = 4
|
||||||
|
) = PinnedMessagesBannerState.Loading(
|
||||||
|
realPinnedMessagesCount = knownPinnedMessagesCount
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun aLoadedPinnedMessagesBannerState(
|
||||||
|
currentPinnedMessageIndex: Int = 0,
|
||||||
|
knownPinnedMessagesCount: Int = 1,
|
||||||
currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem(
|
currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem(
|
||||||
eventId = EventId("\$" + Random.nextInt().toString()),
|
eventId = EventId("\$" + Random.nextInt().toString()),
|
||||||
formatted = AnnotatedString("This is a pinned message")
|
formatted = AnnotatedString("This is a pinned message")
|
||||||
),
|
),
|
||||||
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
eventSink: (PinnedMessagesBannerEvents) -> Unit = {}
|
||||||
) = PinnedMessagesBannerState(
|
) = PinnedMessagesBannerState.Loaded(
|
||||||
pinnedMessagesCount = pinnedMessagesCount,
|
|
||||||
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
|
||||||
currentPinnedMessage = currentPinnedMessage,
|
currentPinnedMessage = currentPinnedMessage,
|
||||||
|
currentPinnedMessageIndex = currentPinnedMessageIndex,
|
||||||
|
knownPinnedMessagesCount = knownPinnedMessagesCount,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -71,42 +71,52 @@ fun PinnedMessagesBannerView(
|
||||||
onViewAllClick: () -> Unit,
|
onViewAllClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
if (state.currentPinnedMessage == null) return
|
Box(modifier = modifier) {
|
||||||
|
when (state) {
|
||||||
|
PinnedMessagesBannerState.Hidden -> Unit
|
||||||
|
is PinnedMessagesBannerState.Loading -> {
|
||||||
|
PinnedMessagesBannerRow(
|
||||||
|
state = state,
|
||||||
|
onViewAllClick = onViewAllClick,
|
||||||
|
modifier = Modifier.clickable(onClick = { }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is PinnedMessagesBannerState.Loaded -> {
|
||||||
|
fun onClick() {
|
||||||
|
onClick(state.currentPinnedMessage.eventId)
|
||||||
|
state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned)
|
||||||
|
}
|
||||||
|
|
||||||
|
PinnedMessagesBannerRow(
|
||||||
|
state = state,
|
||||||
|
onViewAllClick = onViewAllClick,
|
||||||
|
modifier = Modifier.clickable(onClick = ::onClick),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PinnedMessagesBannerRow(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onViewAllClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
val borderColor = ElementTheme.colors.pinnedMessageBannerBorder
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(color = ElementTheme.colors.bgCanvasDefault)
|
.background(color = ElementTheme.colors.bgCanvasDefault)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.drawBehind {
|
.drawBorder(borderColor)
|
||||||
val strokeWidth = 0.5.dp.toPx()
|
.heightIn(min = 64.dp),
|
||||||
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,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = spacedBy(10.dp)
|
horizontalArrangement = spacedBy(10.dp)
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.width(16.dp))
|
Spacer(modifier = Modifier.width(16.dp))
|
||||||
PinIndicators(
|
PinIndicators(
|
||||||
pinIndex = state.currentPinnedMessageIndex,
|
pinIndex = state.currentPinnedMessageIndex(),
|
||||||
pinsCount = state.pinnedMessagesCount,
|
pinsCount = state.pinnedMessagesCount(),
|
||||||
modifier = Modifier.heightIn(max = 40.dp)
|
modifier = Modifier.heightIn(max = 40.dp)
|
||||||
)
|
)
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -116,15 +126,56 @@ fun PinnedMessagesBannerView(
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
PinnedMessageItem(
|
PinnedMessageItem(
|
||||||
index = state.currentPinnedMessageIndex,
|
index = state.currentPinnedMessageIndex(),
|
||||||
totalCount = state.pinnedMessagesCount,
|
totalCount = state.pinnedMessagesCount(),
|
||||||
message = state.currentPinnedMessage.formatted,
|
message = state.formattedMessage(),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = onViewAllClick)
|
ViewAllButton(state, onViewAllClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ViewAllButton(
|
||||||
|
state: PinnedMessagesBannerState,
|
||||||
|
onViewAllClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
val text = if (state is PinnedMessagesBannerState.Loaded) {
|
||||||
|
stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title)
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
text = text,
|
||||||
|
showProgress = state is PinnedMessagesBannerState.Loading,
|
||||||
|
onClick = onViewAllClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.drawBorder(borderColor: Color): Modifier {
|
||||||
|
return this
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PinIndicators(
|
private fun PinIndicators(
|
||||||
pinIndex: Int,
|
pinIndex: Int,
|
||||||
|
|
@ -157,15 +208,15 @@ private fun PinIndicators(
|
||||||
items(pinsCount) { index ->
|
items(pinsCount) { index ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(2.dp)
|
.width(2.dp)
|
||||||
.height(indicatorHeight.dp)
|
.height(indicatorHeight.dp)
|
||||||
.background(
|
.background(
|
||||||
color = if (index == pinIndex) {
|
color = if (index == pinIndex) {
|
||||||
ElementTheme.colors.iconAccentPrimary
|
ElementTheme.colors.iconAccentPrimary
|
||||||
} else {
|
} else {
|
||||||
ElementTheme.colors.pinnedMessageBannerIndicator
|
ElementTheme.colors.pinnedMessageBannerIndicator
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +226,7 @@ private fun PinIndicators(
|
||||||
private fun PinnedMessageItem(
|
private fun PinnedMessageItem(
|
||||||
index: Int,
|
index: Int,
|
||||||
totalCount: Int,
|
totalCount: Int,
|
||||||
message: AnnotatedString,
|
message: AnnotatedString?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount)
|
val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount)
|
||||||
|
|
@ -193,13 +244,15 @@ private fun PinnedMessageItem(
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
if (message != null) {
|
||||||
text = message,
|
Text(
|
||||||
style = ElementTheme.typography.fontBodyMdRegular,
|
text = message,
|
||||||
color = ElementTheme.colors.textPrimary,
|
style = ElementTheme.typography.fontBodyMdRegular,
|
||||||
overflow = TextOverflow.Ellipsis,
|
color = ElementTheme.colors.textPrimary,
|
||||||
maxLines = 1,
|
overflow = TextOverflow.Ellipsis,
|
||||||
)
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import io.element.android.features.messages.impl.fixtures.aMessageEvent
|
||||||
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory
|
||||||
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext
|
||||||
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
|
||||||
import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState
|
import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState
|
||||||
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineController
|
import io.element.android.features.messages.impl.timeline.TimelineController
|
||||||
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
import io.element.android.features.messages.impl.timeline.TimelineItemIndexer
|
||||||
|
|
@ -1072,7 +1072,7 @@ class MessagesPresenterTest {
|
||||||
customReactionPresenter = customReactionPresenter,
|
customReactionPresenter = customReactionPresenter,
|
||||||
reactionSummaryPresenter = reactionSummaryPresenter,
|
reactionSummaryPresenter = reactionSummaryPresenter,
|
||||||
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter,
|
||||||
pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() },
|
pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() },
|
||||||
networkMonitor = FakeNetworkMonitor(),
|
networkMonitor = FakeNetworkMonitor(),
|
||||||
snackbarDispatcher = SnackbarDispatcher(),
|
snackbarDispatcher = SnackbarDispatcher(),
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|
|
||||||
|
|
@ -293,6 +293,7 @@ Reason: %1$s."</string>
|
||||||
<string name="screen_room_member_details_unblock_user">"Unblock user"</string>
|
<string name="screen_room_member_details_unblock_user">"Unblock user"</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_view_all_button_title">"View All"</string>
|
<string name="screen_room_pinned_banner_view_all_button_title">"View All"</string>
|
||||||
<string name="screen_room_title">"Chat"</string>
|
<string name="screen_room_title">"Chat"</string>
|
||||||
<string name="screen_share_location_title">"Share location"</string>
|
<string name="screen_share_location_title">"Share location"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue