Add floating/sticky date badge in the timeline (#6496)
* Add floating date indicator while scrolling the timeline (#6433) * Add `FeatureFlags.FloatingDateBadge`. This enables displaying the floating date badge in the timeline as you scroll. * Don't display the floating badge if the timeline isn't reversed. Otherwise, this will affect talkback users and break the existing navigation * Use `TimelineItem.formattedDate()` to get the date to display. Always try finding the closest one (usually it will be just the 1st one we try). * Align designs with iOS. Also fix shadows in fade animation by adding some paddings. * Update screenshots --------- Co-authored-by: Gianluca Iavicoli <gianluca.iavicoli04@gmail.com> Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
977e64c295
commit
b340e85f83
@@ -39,6 +39,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.RectangleShape
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
import androidx.compose.ui.semantics.Role
|
||||||
@@ -464,6 +465,9 @@ private fun MessagesViewContent(
|
|||||||
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior(
|
val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberScrollBehavior(
|
||||||
pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0,
|
pinnedMessagesCount = (state.pinnedMessagesBannerState as? PinnedMessagesBannerState.Visible)?.pinnedMessagesCount() ?: 0,
|
||||||
)
|
)
|
||||||
|
val density = LocalDensity.current
|
||||||
|
var pinnedBannerHeightDp by remember { mutableStateOf(0.dp) }
|
||||||
|
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = state.timelineState,
|
state = state.timelineState,
|
||||||
timelineProtectionState = state.timelineProtectionState,
|
timelineProtectionState = state.timelineProtectionState,
|
||||||
@@ -479,11 +483,13 @@ private fun MessagesViewContent(
|
|||||||
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
forceJumpToBottomVisibility = forceJumpToBottomVisibility,
|
||||||
onJoinCallClick = onJoinCallClick,
|
onJoinCallClick = onJoinCallClick,
|
||||||
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
nestedScrollConnection = scrollBehavior.nestedScrollConnection,
|
||||||
|
floatingDateTopOffset = pinnedBannerHeightDp,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (state.timelineState.timelineMode !is Timeline.Mode.Thread) {
|
if (state.timelineState.timelineMode !is Timeline.Mode.Thread) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
|
visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible,
|
||||||
|
modifier = Modifier.onSizeChanged { pinnedBannerHeightDp = with(density) { it.height.toDp() } },
|
||||||
enter = expandVertically(),
|
enter = expandVertically(),
|
||||||
exit = shrinkVertically(),
|
exit = shrinkVertically(),
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -149,6 +149,9 @@ class TimelinePresenter(
|
|||||||
val displayThreadSummaries by produceState(false) {
|
val displayThreadSummaries by produceState(false) {
|
||||||
value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
value = featureFlagService.isFeatureEnabled(FeatureFlags.Threads)
|
||||||
}
|
}
|
||||||
|
val displayFloatingDateBadge by produceState(false) {
|
||||||
|
value = featureFlagService.isFeatureEnabled(FeatureFlags.FloatingDateBadge)
|
||||||
|
}
|
||||||
|
|
||||||
fun handleEvent(event: TimelineEvent) {
|
fun handleEvent(event: TimelineEvent) {
|
||||||
when (event) {
|
when (event) {
|
||||||
@@ -315,6 +318,7 @@ class TimelinePresenter(
|
|||||||
messageShieldDialogData = messageShieldDialogData.value,
|
messageShieldDialogData = messageShieldDialogData.value,
|
||||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
displayThreadSummaries = displayThreadSummaries,
|
displayThreadSummaries = displayThreadSummaries,
|
||||||
|
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||||
eventSink = ::handleEvent,
|
eventSink = ::handleEvent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ data class TimelineState(
|
|||||||
val messageShieldDialogData: MessageShieldData?,
|
val messageShieldDialogData: MessageShieldData?,
|
||||||
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
val resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState,
|
||||||
val displayThreadSummaries: Boolean,
|
val displayThreadSummaries: Boolean,
|
||||||
|
val displayFloatingDateBadge: Boolean,
|
||||||
val eventSink: (TimelineEvent) -> Unit,
|
val eventSink: (TimelineEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
|
private val lastTimelineEvent = timelineItems.firstOrNull { it is TimelineItem.Event } as? TimelineItem.Event
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fun aTimelineState(
|
|||||||
messageShield: MessageShield? = null,
|
messageShield: MessageShield? = null,
|
||||||
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
|
resolveVerifiedUserSendFailureState: ResolveVerifiedUserSendFailureState = aResolveVerifiedUserSendFailureState(),
|
||||||
displayThreadSummaries: Boolean = false,
|
displayThreadSummaries: Boolean = false,
|
||||||
|
displayFloatingDateBadge: Boolean = false,
|
||||||
eventSink: (TimelineEvent) -> Unit = {},
|
eventSink: (TimelineEvent) -> Unit = {},
|
||||||
): TimelineState {
|
): TimelineState {
|
||||||
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
val focusedEventId = timelineItems.filterIsInstance<TimelineItem.Event>().getOrNull(focusedEventIndex)?.eventId
|
||||||
@@ -75,6 +76,7 @@ fun aTimelineState(
|
|||||||
messageShieldDialogData = messageShield?.let { MessageShieldData(it) },
|
messageShieldDialogData = messageShield?.let { MessageShieldData(it) },
|
||||||
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
resolveVerifiedUserSendFailureState = resolveVerifiedUserSendFailureState,
|
||||||
displayThreadSummaries = displayThreadSummaries,
|
displayThreadSummaries = displayThreadSummaries,
|
||||||
|
displayFloatingDateBadge = displayFloatingDateBadge,
|
||||||
eventSink = eventSink,
|
eventSink = eventSink,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ import androidx.compose.ui.platform.LocalView
|
|||||||
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.compound.theme.ElementTheme
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||||
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView
|
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView
|
||||||
|
import io.element.android.features.messages.impl.timeline.components.FloatingDateBadgeOverlay
|
||||||
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
import io.element.android.features.messages.impl.timeline.components.TimelineItemRow
|
||||||
import io.element.android.features.messages.impl.timeline.components.toText
|
import io.element.android.features.messages.impl.timeline.components.toText
|
||||||
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
import io.element.android.features.messages.impl.timeline.di.LocalTimelineItemPresenterFactories
|
||||||
@@ -105,6 +107,7 @@ fun TimelineView(
|
|||||||
lazyListState: LazyListState = rememberLazyListState(),
|
lazyListState: LazyListState = rememberLazyListState(),
|
||||||
forceJumpToBottomVisibility: Boolean = false,
|
forceJumpToBottomVisibility: Boolean = false,
|
||||||
nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
|
nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(),
|
||||||
|
floatingDateTopOffset: Dp = 0.dp,
|
||||||
) {
|
) {
|
||||||
fun clearFocusRequestState() {
|
fun clearFocusRequestState() {
|
||||||
state.eventSink(TimelineEvent.ClearFocusRequestState)
|
state.eventSink(TimelineEvent.ClearFocusRequestState)
|
||||||
@@ -210,6 +213,15 @@ fun TimelineView(
|
|||||||
onJumpToLive = ::onJumpToLive,
|
onJumpToLive = ::onJumpToLive,
|
||||||
onFocusEventRender = ::onFocusEventRender,
|
onFocusEventRender = ::onFocusEventRender,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (state.displayFloatingDateBadge && useReverseLayout) {
|
||||||
|
FloatingDateBadgeOverlay(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
timelineItems = state.timelineItems,
|
||||||
|
isLive = state.isLive,
|
||||||
|
topOffset = floatingDateTopOffset,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.features.messages.impl.timeline.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.TimelineItem
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
|
import io.element.android.libraries.designsystem.theme.floatingDateBadgeBackground
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun BoxScope.FloatingDateBadgeOverlay(
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
timelineItems: ImmutableList<TimelineItem>,
|
||||||
|
isLive: Boolean,
|
||||||
|
topOffset: Dp = 0.dp,
|
||||||
|
) {
|
||||||
|
// This needs to be a state to trigger a `derivedState` recalculation
|
||||||
|
val updatedTimelineItems by rememberUpdatedState(timelineItems)
|
||||||
|
|
||||||
|
// Look for the last visible item with a timestamp, starting from the last visible item and going backwards until we find one or reach the start of the list
|
||||||
|
val lastVisibleItemWithTimestamp by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
var index = lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: return@derivedStateOf null
|
||||||
|
while (index >= 0) {
|
||||||
|
when (val item = updatedTimelineItems.getOrNull(index)) {
|
||||||
|
is TimelineItem.Event -> return@derivedStateOf item
|
||||||
|
is TimelineItem.Virtual -> if (item.model is TimelineItemDaySeparatorModel) return@derivedStateOf item
|
||||||
|
is TimelineItem.GroupedEvents -> return@derivedStateOf item.events.firstOrNull()
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
index--
|
||||||
|
}
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the formatted date so we recompute it lazily and can keep it around even if we need to dispose the badge because the timeline items changed
|
||||||
|
var formattedDate: String? by remember { mutableStateOf(null) }
|
||||||
|
// Update the formatted date when we have a new non-null timestamp
|
||||||
|
LaunchedEffect(lastVisibleItemWithTimestamp) {
|
||||||
|
lastVisibleItemWithTimestamp?.formattedDate()?.let { formattedDate = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val isAtBottom by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
lazyListState.firstVisibleItemIndex < 3 && isLive
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var isBadgeVisible by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { lazyListState.isScrollInProgress }
|
||||||
|
.collectLatest { isScrolling ->
|
||||||
|
if (isScrolling) {
|
||||||
|
isBadgeVisible = true
|
||||||
|
} else {
|
||||||
|
delay(2000.milliseconds)
|
||||||
|
isBadgeVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val showBadge = isBadgeVisible && !isAtBottom && formattedDate != null
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = showBadge,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = 8.dp + topOffset),
|
||||||
|
enter = fadeIn(animationSpec = tween(150)),
|
||||||
|
exit = fadeOut(animationSpec = tween(300)),
|
||||||
|
) {
|
||||||
|
formattedDate?.let { dateText ->
|
||||||
|
FloatingDateBadge(
|
||||||
|
modifier = Modifier.padding(8.dp),
|
||||||
|
dateText = dateText,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun FloatingDateBadge(
|
||||||
|
dateText: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = ElementTheme.colors.floatingDateBadgeBackground,
|
||||||
|
shadowElevation = 4.dp,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
text = dateText,
|
||||||
|
style = ElementTheme.typography.fontBodyMdMedium,
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun FloatingDateBadgePreview() = ElementPreview {
|
||||||
|
Box(modifier = Modifier.padding(16.dp)) {
|
||||||
|
FloatingDateBadge(dateText = "March 9, 2026")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,11 @@ class TimelineItemEventFactory(
|
|||||||
timestamp = currentTimelineItem.event.timestamp,
|
timestamp = currentTimelineItem.event.timestamp,
|
||||||
mode = DateFormatterMode.TimeOnly,
|
mode = DateFormatterMode.TimeOnly,
|
||||||
)
|
)
|
||||||
|
val sentDate = dateFormatter.format(
|
||||||
|
timestamp = currentTimelineItem.event.timestamp,
|
||||||
|
mode = DateFormatterMode.Day,
|
||||||
|
useRelative = true,
|
||||||
|
)
|
||||||
val senderAvatarData = AvatarData(
|
val senderAvatarData = AvatarData(
|
||||||
id = currentSender.value,
|
id = currentSender.value,
|
||||||
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
name = senderProfile.getDisambiguatedDisplayName(currentSender),
|
||||||
@@ -108,6 +113,7 @@ class TimelineItemEventFactory(
|
|||||||
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
|
||||||
sentTimeMillis = currentTimelineItem.event.timestamp,
|
sentTimeMillis = currentTimelineItem.event.timestamp,
|
||||||
sentTime = sentTime,
|
sentTime = sentTime,
|
||||||
|
sentDate = sentDate,
|
||||||
groupPosition = groupPosition,
|
groupPosition = groupPosition,
|
||||||
reactionsState = currentTimelineItem.computeReactionsState(),
|
reactionsState = currentTimelineItem.computeReactionsState(),
|
||||||
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
readReceiptState = currentTimelineItem.computeReadReceiptState(roomMembers),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt
|
|||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStickerContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent
|
||||||
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent
|
||||||
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
|
||||||
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
|
||||||
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
import io.element.android.libraries.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
@@ -59,6 +60,12 @@ sealed interface TimelineItem {
|
|||||||
is GroupedEvents -> "groupedEvent"
|
is GroupedEvents -> "groupedEvent"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun formattedDate(): String? = when (this) {
|
||||||
|
is Event -> sentDate.takeIf { it.isNotEmpty() }
|
||||||
|
is Virtual -> (model as? TimelineItemDaySeparatorModel)?.formattedDate?.takeIf { it.isNotEmpty() }
|
||||||
|
is GroupedEvents -> null
|
||||||
|
}
|
||||||
|
|
||||||
data class Virtual(
|
data class Virtual(
|
||||||
val id: UniqueId,
|
val id: UniqueId,
|
||||||
val model: TimelineItemVirtualModel
|
val model: TimelineItemVirtualModel
|
||||||
@@ -75,6 +82,7 @@ sealed interface TimelineItem {
|
|||||||
val content: TimelineItemEventContent,
|
val content: TimelineItemEventContent,
|
||||||
val sentTimeMillis: Long = 0L,
|
val sentTimeMillis: Long = 0L,
|
||||||
val sentTime: String = "",
|
val sentTime: String = "",
|
||||||
|
val sentDate: String = "",
|
||||||
val isMine: Boolean = false,
|
val isMine: Boolean = false,
|
||||||
val isEditable: Boolean,
|
val isEditable: Boolean,
|
||||||
val canBeRepliedTo: Boolean,
|
val canBeRepliedTo: Boolean,
|
||||||
|
|||||||
@@ -75,6 +75,9 @@ val SemanticColors.pinnedMessageBannerIndicator
|
|||||||
val SemanticColors.pinnedMessageBannerBorder
|
val SemanticColors.pinnedMessageBannerBorder
|
||||||
get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400
|
get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400
|
||||||
|
|
||||||
|
val SemanticColors.floatingDateBadgeBackground
|
||||||
|
get() = if (isLight) bgCanvasDefault else bgSubtlePrimary
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ColorAliasesPreview() = ElementPreview {
|
internal fun ColorAliasesPreview() = ElementPreview {
|
||||||
|
|||||||
@@ -156,10 +156,17 @@ enum class FeatureFlags(
|
|||||||
),
|
),
|
||||||
ValidateNetworkWhenSchedulingNotificationFetching(
|
ValidateNetworkWhenSchedulingNotificationFetching(
|
||||||
key = "feature.validate_network_when_scheduling_notification_fetching",
|
key = "feature.validate_network_when_scheduling_notification_fetching",
|
||||||
title = "validate internet connectivity when scheduling notification fetching",
|
title = "Validate internet connectivity when scheduling notification fetching",
|
||||||
description = "Only fetch events for push notifications when the device has internet connectivity. " +
|
description = "Only fetch events for push notifications when the device has internet connectivity. " +
|
||||||
"Enabling this can be problematic in air-gapped environments.",
|
"Enabling this can be problematic in air-gapped environments.",
|
||||||
defaultValue = { true },
|
defaultValue = { true },
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
),
|
),
|
||||||
|
FloatingDateBadge(
|
||||||
|
key = "feature.floating_date_badge",
|
||||||
|
title = "Display sticky date headers in the timeline",
|
||||||
|
description = "When scrolling, a sticky date badge will be displayed so you can easily know on which date the messages you're seeing were sent.",
|
||||||
|
defaultValue = { false },
|
||||||
|
isFinished = false,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user