Skip to content

Commit 784f5df

Browse files
committed
Mark as read menu ui tweaks
1 parent 880a189 commit 784f5df

1 file changed

Lines changed: 90 additions & 31 deletions

File tree

  • features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ package io.element.android.features.messages.impl.timeline
1010

1111
import android.view.HapticFeedbackConstants
1212
import androidx.compose.animation.AnimatedVisibility
13+
import androidx.compose.animation.core.MutableTransitionState
1314
import androidx.compose.animation.core.tween
1415
import androidx.compose.animation.fadeIn
16+
import androidx.compose.animation.fadeOut
1517
import androidx.compose.animation.scaleIn
1618
import androidx.compose.animation.scaleOut
1719
import androidx.compose.foundation.background
@@ -34,6 +36,7 @@ import androidx.compose.foundation.lazy.LazyListState
3436
import androidx.compose.foundation.lazy.items
3537
import androidx.compose.foundation.lazy.rememberLazyListState
3638
import androidx.compose.foundation.shape.CircleShape
39+
import androidx.compose.foundation.shape.RoundedCornerShape
3740
import androidx.compose.runtime.Composable
3841
import androidx.compose.runtime.CompositionLocalProvider
3942
import androidx.compose.runtime.LaunchedEffect
@@ -48,18 +51,26 @@ import androidx.compose.runtime.snapshotFlow
4851
import androidx.compose.ui.Alignment
4952
import androidx.compose.ui.Modifier
5053
import androidx.compose.ui.draw.clip
54+
import androidx.compose.ui.draw.shadow
55+
import androidx.compose.ui.graphics.TransformOrigin
5156
import androidx.compose.ui.graphics.vector.ImageVector
5257
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
5358
import androidx.compose.ui.input.nestedscroll.nestedScroll
5459
import androidx.compose.ui.platform.LocalContext
60+
import androidx.compose.ui.platform.LocalDensity
5561
import androidx.compose.ui.platform.LocalView
5662
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
5763
import androidx.compose.ui.res.stringResource
5864
import androidx.compose.ui.tooling.preview.PreviewParameter
5965
import androidx.compose.ui.unit.Dp
60-
import androidx.compose.ui.unit.DpOffset
6166
import androidx.compose.ui.unit.IntOffset
67+
import androidx.compose.ui.unit.IntRect
68+
import androidx.compose.ui.unit.IntSize
69+
import androidx.compose.ui.unit.LayoutDirection
6270
import androidx.compose.ui.unit.dp
71+
import androidx.compose.ui.window.Popup
72+
import androidx.compose.ui.window.PopupPositionProvider
73+
import androidx.compose.ui.window.PopupProperties
6374
import io.element.android.compound.theme.ElementTheme
6475
import io.element.android.compound.tokens.generated.CompoundIcons
6576
import io.element.android.features.messages.impl.crypto.sendfailure.resolve.ResolveVerifiedUserSendFailureView
@@ -80,7 +91,6 @@ import io.element.android.libraries.designsystem.components.dialogs.AlertDialog
8091
import io.element.android.libraries.designsystem.preview.ElementPreview
8192
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
8293
import io.element.android.libraries.designsystem.text.roundToPx
83-
import io.element.android.libraries.designsystem.theme.components.DropdownMenu
8494
import io.element.android.libraries.designsystem.theme.components.Icon
8595
import io.element.android.libraries.designsystem.theme.components.Text
8696
import io.element.android.libraries.designsystem.utils.animateScrollToItemCenter
@@ -448,8 +458,8 @@ private fun JumpToPositionButton(
448458
AnimatedVisibility(
449459
modifier = modifier,
450460
visible = isVisible,
451-
enter = scaleIn(animationSpec = tween(100)),
452-
exit = scaleOut(animationSpec = tween(100)),
461+
enter = scaleIn(animationSpec = tween(220), initialScale = 0.8f) + fadeIn(animationSpec = tween(220)),
462+
exit = scaleOut(animationSpec = tween(180), targetScale = 0.8f) + fadeOut(animationSpec = tween(180)),
453463
) {
454464
var menuExpanded by remember { mutableStateOf(false) }
455465
Box {
@@ -473,34 +483,60 @@ private fun JumpToPositionButton(
473483
contentDescription = contentDescription,
474484
tint = ElementTheme.colors.iconSecondary,
475485
)
476-
DropdownMenu(
477-
expanded = menuExpanded,
478-
onDismissRequest = { menuExpanded = false },
479-
minWidth = 0.dp,
480-
offset = DpOffset(x = -44.dp, y = 40.dp)
481-
) {
482-
// Hand-rolled instead of DropdownMenuItem: padding here is tighter
483-
// than DropdownMenuItem's 12.dp default to match the Figma spec.
484-
Row(
485-
modifier = Modifier
486-
.clickable {
487-
menuExpanded = false
488-
onMarkAsRead()
489-
}
490-
.padding(horizontal = 12.dp, vertical = 8.dp),
491-
verticalAlignment = Alignment.CenterVertically,
486+
val menuTransitionState = remember { MutableTransitionState(false) }
487+
.apply { targetState = menuExpanded }
488+
if (menuTransitionState.currentState || menuTransitionState.targetState) {
489+
val gapPx = with(LocalDensity.current) { 8.dp.roundToPx() }
490+
val positionProvider = remember(gapPx) { CenterStartOfAnchorPositionProvider(gapPx) }
491+
Popup(
492+
popupPositionProvider = positionProvider,
493+
onDismissRequest = { menuExpanded = false },
494+
properties = PopupProperties(focusable = true),
492495
) {
493-
Icon(
494-
imageVector = CompoundIcons.MarkAsRead(),
495-
contentDescription = null,
496-
tint = ElementTheme.colors.iconPrimary,
497-
)
498-
Spacer(modifier = Modifier.width(8.dp))
499-
Text(
500-
text = stringResource(id = CommonStrings.action_mark_as_read),
501-
color = ElementTheme.colors.textPrimary,
502-
style = ElementTheme.typography.fontBodyLgRegular,
503-
)
496+
// Anchor the scale to the right-center edge so the menu visually grows
497+
// outward from the FAB it's attached to.
498+
val transformOrigin = TransformOrigin(pivotFractionX = 1f, pivotFractionY = 0.5f)
499+
AnimatedVisibility(
500+
visibleState = menuTransitionState,
501+
enter = scaleIn(
502+
animationSpec = tween(180),
503+
initialScale = 0.9f,
504+
transformOrigin = transformOrigin,
505+
) + fadeIn(animationSpec = tween(180)),
506+
exit = scaleOut(
507+
animationSpec = tween(140),
508+
targetScale = 0.9f,
509+
transformOrigin = transformOrigin,
510+
) + fadeOut(animationSpec = tween(140)),
511+
) {
512+
// Hand-rolled instead of DropdownMenuItem: padding here is tighter
513+
// than DropdownMenuItem's 12.dp default to match the Figma spec.
514+
Row(
515+
modifier = Modifier
516+
.shadow(elevation = 1.dp, shape = RoundedCornerShape(8.dp))
517+
.clip(RoundedCornerShape(8.dp))
518+
.background(ElementTheme.colors.bgCanvasDefaultLevel1)
519+
.border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp))
520+
.clickable {
521+
menuExpanded = false
522+
onMarkAsRead()
523+
}
524+
.padding(horizontal = 12.dp, vertical = 12.dp),
525+
verticalAlignment = Alignment.CenterVertically,
526+
) {
527+
Icon(
528+
imageVector = CompoundIcons.MarkAsRead(),
529+
contentDescription = null,
530+
tint = ElementTheme.colors.iconTertiary,
531+
)
532+
Spacer(modifier = Modifier.width(8.dp))
533+
Text(
534+
text = stringResource(id = CommonStrings.action_mark_as_read),
535+
color = ElementTheme.colors.textPrimary,
536+
style = ElementTheme.typography.fontBodyLgRegular,
537+
)
538+
}
539+
}
504540
}
505541
}
506542
}
@@ -617,3 +653,26 @@ internal fun TimelineViewWithReadMarkerNoIndicatorsPreview() = ElementPreview {
617653
internal fun TimelineViewWithReadMarkerBothIndicatorsPreview() = ElementPreview {
618654
TimelineViewWithReadMarker(hasUnreadAbove = true, hasUnreadBelow = true)
619655
}
656+
657+
/**
658+
* Anchors the popup so its right edge sits [gapPx] to the left of the anchor and its vertical
659+
* center matches the anchor's. Adapts to localized menu widths and FAB size; coerced to stay
660+
* on-screen.
661+
*/
662+
private class CenterStartOfAnchorPositionProvider(
663+
private val gapPx: Int,
664+
) : PopupPositionProvider {
665+
override fun calculatePosition(
666+
anchorBounds: IntRect,
667+
windowSize: IntSize,
668+
layoutDirection: LayoutDirection,
669+
popupContentSize: IntSize,
670+
): IntOffset {
671+
val x = anchorBounds.left - popupContentSize.width - gapPx
672+
val y = anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2
673+
return IntOffset(
674+
x = x.coerceIn(0, (windowSize.width - popupContentSize.width).coerceAtLeast(0)),
675+
y = y.coerceIn(0, (windowSize.height - popupContentSize.height).coerceAtLeast(0)),
676+
)
677+
}
678+
}

0 commit comments

Comments
 (0)