@@ -87,6 +87,7 @@ import androidx.compose.ui.unit.IntOffset
8787import androidx.compose.ui.unit.dp
8888import java.util.Locale
8989import kotlin.math.PI
90+ import kotlin.math.abs
9091import kotlin.math.roundToInt
9192import kotlin.math.sin
9293import kotlinx.coroutines.Dispatchers
@@ -732,7 +733,14 @@ private fun LiveFeedSheet(
732733 onDispatch : (SentryBuddySessionController .() -> Unit ) -> Unit ,
733734) {
734735 SheetTitle (" Sentry Buddy" , " Live Feed" )
735- AttentionCard (liveFeed, sentryUiLinks, nowMs)
736+ val emptyAttentionArtIndex = remember { EmptyAttentionArtIndex .next() }
737+ AttentionCard (
738+ liveFeed = liveFeed,
739+ sentryUiLinks = sentryUiLinks,
740+ nowMs = nowMs,
741+ emptyArtIndex = emptyAttentionArtIndex,
742+ onDismiss = { onDispatch { dismissLiveFeedAttention() } },
743+ )
736744 Button (
737745 modifier = Modifier .fillMaxWidth().height(56 .dp),
738746 colors = ButtonDefaults .buttonColors(containerColor = BuddyPurple ),
@@ -754,86 +762,206 @@ private fun LiveFeedSheet(
754762}
755763
756764@Composable
757- private fun AttentionCard (liveFeed : BuddyLiveFeed , sentryUiLinks : BuddySentryUiLinks , nowMs : Long ) {
765+ private fun AttentionCard (
766+ liveFeed : BuddyLiveFeed ,
767+ sentryUiLinks : BuddySentryUiLinks ,
768+ nowMs : Long ,
769+ emptyArtIndex : Int ,
770+ onDismiss : () -> Unit ,
771+ ) {
772+ val item = liveFeed.latestUnviewedAdverseItem
773+ val dismissOffset = remember(item?.id) { Animatable (0f ) }
774+ val headerAlpha =
775+ if (item == null ) {
776+ 0f
777+ } else {
778+ (1f - (- dismissOffset.value / ATTENTION_HEADER_FADE_DISTANCE_PX )).coerceIn(0f , 1f )
779+ }
758780 Text (
759781 " Needs attention" ,
760782 style = MaterialTheme .typography.titleMedium,
761783 fontWeight = FontWeight .Bold ,
762- color = BuddyInk ,
784+ color = BuddyInk .copy(alpha = headerAlpha) ,
763785 )
764786
765- val item = liveFeed.latestAdverseItem
766787 if (item == null ) {
767- Row (
768- modifier = Modifier .fillMaxWidth().padding(vertical = 6 .dp),
769- horizontalArrangement = Arrangement .SpaceBetween ,
770- verticalAlignment = Alignment .CenterVertically ,
788+ Surface (
789+ modifier = Modifier .fillMaxWidth(),
790+ color = Color .Transparent ,
791+ shape = RoundedCornerShape (16 .dp),
792+ border = CardDefaults .outlinedCardBorder(),
771793 ) {
772- Text (" No recent errors or slow spans." , color = BuddyMuted )
773- Text (" OK" , color = BuddyPurple , fontWeight = FontWeight .Bold )
794+ EmptyAttentionArt (
795+ index = emptyArtIndex,
796+ modifier = Modifier .fillMaxWidth().height(132 .dp).padding(18 .dp),
797+ )
774798 }
775799 return
776800 }
777801
778802 val color = severityColor(item.severity)
779803 val context = LocalContext .current
780804 val link = sentryUiLinks.linkFor(item)
781- Card (
782- modifier =
783- Modifier .clickable(enabled = link != null ) { link?.let { openSentryLink(context, it) } },
784- colors = CardDefaults .cardColors(containerColor = color.copy(alpha = 0.10f )),
805+ val dismissScope = rememberCoroutineScope()
806+ Surface (
807+ modifier = Modifier .fillMaxWidth(),
808+ color = Color .Transparent ,
809+ shape = RoundedCornerShape (16 .dp),
810+ border = CardDefaults .outlinedCardBorder(),
785811 ) {
786- Column (
787- modifier = Modifier .fillMaxWidth().padding(16 .dp),
788- verticalArrangement = Arrangement .spacedBy(10 .dp),
812+ BoxWithConstraints (
813+ modifier =
814+ Modifier .fillMaxWidth().heightIn(min = 132 .dp).pointerInput(item.id) {
815+ detectDragGestures(
816+ onDragEnd = {
817+ val dismissDistance = size.width.toFloat()
818+ val shouldDismiss = abs(dismissOffset.value) > dismissDistance * 0.35f
819+ dismissScope.launch {
820+ if (shouldDismiss) {
821+ dismissOffset.animateTo(- dismissDistance)
822+ onDismiss()
823+ } else {
824+ dismissOffset.animateTo(0f )
825+ }
826+ }
827+ },
828+ onDragCancel = { dismissScope.launch { dismissOffset.animateTo(0f ) } },
829+ ) { change, dragAmount ->
830+ change.consume()
831+ val dismissDistance = size.width.toFloat()
832+ val nextOffset = (dismissOffset.value + dragAmount.x).coerceIn(- dismissDistance, 0f )
833+ dismissScope.launch { dismissOffset.snapTo(nextOffset) }
834+ }
835+ }
789836 ) {
790- Row (
791- modifier = Modifier .fillMaxWidth(),
792- horizontalArrangement = Arrangement .spacedBy(10 .dp),
793- verticalAlignment = Alignment .CenterVertically ,
837+ val widthPx = with (LocalDensity .current) { maxWidth.toPx() }
838+ Box (
839+ modifier =
840+ Modifier .matchParentSize().offset {
841+ IntOffset ((dismissOffset.value + widthPx).roundToInt(), 0 )
842+ }
794843 ) {
795- LiveFeedCategoryPill (item.category.label, color)
796- Text (
797- item.title(),
798- modifier = Modifier .weight(1f ),
799- color = BuddyInk ,
800- style = MaterialTheme .typography.titleMedium,
801- fontWeight = FontWeight .Bold ,
802- )
803- Text (
804- relativeTime(item.timestamp.time, nowMs),
805- color = BuddyMuted ,
806- style = MaterialTheme .typography.labelMedium,
844+ EmptyAttentionArt (
845+ index = emptyArtIndex,
846+ modifier = Modifier .fillMaxSize().padding(18 .dp),
807847 )
808848 }
809- item.screenContextText()?.let { screenContext ->
810- Text (
811- screenContext,
812- color = BuddyMuted ,
813- style = MaterialTheme .typography.bodySmall,
849+ Box (
850+ modifier =
851+ Modifier .matchParentSize()
852+ .offset { IntOffset (dismissOffset.value.roundToInt(), 0 ) }
853+ .clickable(enabled = link != null ) { link?.let { openSentryLink(context, it) } }
854+ ) {
855+ AttentionItemContent (
856+ item = item,
857+ liveFeed = liveFeed,
858+ color = color,
859+ nowMs = nowMs,
814860 )
815861 }
816- Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
817- adverseCountChips(liveFeed).forEach { chip ->
818- Surface (
819- color = Color .White ,
820- shape = RoundedCornerShape (16 .dp),
821- border = CardDefaults .outlinedCardBorder(),
822- ) {
823- Text (
824- chip,
825- modifier = Modifier .padding(horizontal = 10 .dp, vertical = 6 .dp),
826- color = BuddyInk ,
827- style = MaterialTheme .typography.labelMedium,
828- fontWeight = FontWeight .Bold ,
829- )
830- }
862+ }
863+ }
864+ }
865+
866+ @Composable
867+ private fun AttentionItemContent (
868+ item : BuddyLiveFeedItem ,
869+ liveFeed : BuddyLiveFeed ,
870+ color : Color ,
871+ nowMs : Long ,
872+ ) {
873+ Column (
874+ modifier = Modifier .fillMaxWidth().padding(16 .dp),
875+ verticalArrangement = Arrangement .spacedBy(10 .dp),
876+ ) {
877+ Row (
878+ modifier = Modifier .fillMaxWidth(),
879+ horizontalArrangement = Arrangement .spacedBy(10 .dp),
880+ verticalAlignment = Alignment .CenterVertically ,
881+ ) {
882+ LiveFeedCategoryPill (item.category.label, color)
883+ Text (
884+ item.title(),
885+ modifier = Modifier .weight(1f ),
886+ color = BuddyInk ,
887+ style = MaterialTheme .typography.titleMedium,
888+ fontWeight = FontWeight .Normal ,
889+ )
890+ Text (
891+ relativeTime(item.timestamp.time, nowMs),
892+ color = BuddyMuted ,
893+ style = MaterialTheme .typography.labelMedium,
894+ fontWeight = FontWeight .Normal ,
895+ )
896+ }
897+ item.screenContextText()?.let { screenContext ->
898+ Text (
899+ screenContext,
900+ color = BuddyMuted ,
901+ style = MaterialTheme .typography.bodySmall,
902+ fontWeight = FontWeight .Normal ,
903+ )
904+ }
905+ Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
906+ adverseCountChips(liveFeed).forEach { chip ->
907+ Surface (
908+ color = Color .White ,
909+ shape = RoundedCornerShape (16 .dp),
910+ border = CardDefaults .outlinedCardBorder(),
911+ ) {
912+ Text (
913+ chip,
914+ modifier = Modifier .padding(horizontal = 10 .dp, vertical = 6 .dp),
915+ color = BuddyInk ,
916+ style = MaterialTheme .typography.labelMedium,
917+ fontWeight = FontWeight .Normal ,
918+ )
831919 }
832920 }
833921 }
834922 }
835923}
836924
925+ @Composable
926+ private fun EmptyAttentionArt (index : Int , modifier : Modifier = Modifier ) {
927+ Canvas (modifier = modifier) {
928+ val variant = index % EMPTY_ATTENTION_ART_VARIANTS
929+ val palette =
930+ when (variant % 5 ) {
931+ 0 -> listOf (BuddyPurple , BuddyRed , BuddyGold )
932+ 1 -> listOf (BuddyRed , BuddyPurple , BuddyMuted )
933+ 2 -> listOf (BuddyGold , BuddyPurple , BuddyRed )
934+ 3 -> listOf (BuddyPurple , BuddyMuted , BuddyGold )
935+ else -> listOf (BuddyMuted , BuddyRed , BuddyPurple )
936+ }
937+ val w = size.width
938+ val h = size.height
939+ val center = Offset (w * (0.46f + (variant % 3 ) * 0.04f ), h * 0.50f )
940+ drawCircle(palette[0 ].copy(alpha = 0.12f ), radius = h * 0.46f , center = center)
941+ drawCircle(
942+ palette[1 ].copy(alpha = 0.16f ),
943+ radius = h * 0.26f ,
944+ center = Offset (w * 0.68f , h * 0.34f ),
945+ )
946+ drawCircle(
947+ palette[2 ].copy(alpha = 0.18f ),
948+ radius = h * 0.18f ,
949+ center = Offset (w * 0.28f , h * 0.72f ),
950+ )
951+ val glyph =
952+ Path ().apply {
953+ moveTo(w * 0.45f , h * 0.18f )
954+ lineTo(w * 0.27f , h * 0.74f )
955+ lineTo(w * 0.72f , h * 0.74f )
956+ close()
957+ }
958+ drawPath(glyph, palette[0 ].copy(alpha = 0.20f ))
959+ drawLine(palette[0 ], Offset (w * 0.38f , h * 0.58f ), Offset (w * 0.58f , h * 0.58f ), 5f )
960+ drawLine(palette[1 ], Offset (w * 0.42f , h * 0.46f ), Offset (w * 0.62f , h * 0.46f ), 4f )
961+ drawLine(palette[2 ], Offset (w * 0.46f , h * 0.34f ), Offset (w * 0.66f , h * 0.34f ), 3f )
962+ }
963+ }
964+
837965@Composable
838966private fun EmptyLiveFeedCard () {
839967 Card (border = CardDefaults .outlinedCardBorder()) {
@@ -852,31 +980,39 @@ private fun LiveFeedRows(
852980 nowMs : Long ,
853981) {
854982 val context = LocalContext .current
855- Column (verticalArrangement = Arrangement .spacedBy(8 .dp)) {
856- items.forEach { item ->
857- val color =
858- if (item.adverse) severityColor(item.severity) else timelineColor(item.timelineItem)
859- val link = sentryUiLinks.linkFor(item)
860- Row (
861- modifier =
862- Modifier .fillMaxWidth()
863- .clickable(enabled = link != null ) { link?.let { openSentryLink(context, it) } }
864- .padding(vertical = 7 .dp),
865- horizontalArrangement = Arrangement .spacedBy(10 .dp),
866- verticalAlignment = Alignment .CenterVertically ,
867- ) {
868- LiveFeedCategoryPill (item.category.label, color)
869- Text (
870- item.title(),
871- modifier = Modifier .weight(1f ),
872- color = BuddyInk ,
873- fontWeight = if (item.adverse) FontWeight .Bold else FontWeight .Normal ,
874- )
875- Text (
876- relativeTime(item.timestamp.time, nowMs),
877- color = BuddyMuted ,
878- style = MaterialTheme .typography.labelMedium,
879- )
983+ Surface (
984+ modifier = Modifier .fillMaxWidth(),
985+ color = Color .Transparent ,
986+ shape = RoundedCornerShape (16 .dp),
987+ border = CardDefaults .outlinedCardBorder(),
988+ ) {
989+ Column (modifier = Modifier .padding(horizontal = 12 .dp, vertical = 8 .dp)) {
990+ items.forEach { item ->
991+ val color =
992+ if (item.adverse) severityColor(item.severity) else timelineColor(item.timelineItem)
993+ val link = sentryUiLinks.linkFor(item)
994+ Row (
995+ modifier =
996+ Modifier .fillMaxWidth()
997+ .clickable(enabled = link != null ) { link?.let { openSentryLink(context, it) } }
998+ .padding(vertical = 7 .dp),
999+ horizontalArrangement = Arrangement .spacedBy(10 .dp),
1000+ verticalAlignment = Alignment .CenterVertically ,
1001+ ) {
1002+ LiveFeedCategoryPill (item.category.label, color)
1003+ Text (
1004+ item.title(),
1005+ modifier = Modifier .weight(1f ),
1006+ color = BuddyInk ,
1007+ fontWeight = FontWeight .Normal ,
1008+ )
1009+ Text (
1010+ relativeTime(item.timestamp.time, nowMs),
1011+ color = BuddyMuted ,
1012+ style = MaterialTheme .typography.labelMedium,
1013+ fontWeight = FontWeight .Normal ,
1014+ )
1015+ }
8801016 }
8811017 }
8821018 }
@@ -890,11 +1026,21 @@ private fun LiveFeedCategoryPill(label: String, color: Color) {
8901026 modifier = Modifier .padding(horizontal = 9 .dp, vertical = 5 .dp),
8911027 color = color,
8921028 style = MaterialTheme .typography.labelSmall,
893- fontWeight = FontWeight .Bold ,
1029+ fontWeight = FontWeight .Normal ,
8941030 )
8951031 }
8961032}
8971033
1034+ private object EmptyAttentionArtIndex {
1035+ private var nextIndex = 0
1036+
1037+ fun next (): Int {
1038+ val index = nextIndex
1039+ nextIndex = (nextIndex + 1 ) % EMPTY_ATTENTION_ART_VARIANTS
1040+ return index
1041+ }
1042+ }
1043+
8981044@Composable
8991045private fun SentryBuddyGlyph (tint : Color , modifier : Modifier = Modifier ) {
9001046 Icon (
@@ -1427,6 +1573,8 @@ private val BuddyBubbleTouchPadding = 20.dp
14271573private val BuddyTransientTextWidth = 190 .dp
14281574private val BuddyTransientTextHeight = 28 .dp
14291575private const val LIVE_FEED_VISIBLE_ITEM_LIMIT = 7
1576+ private const val EMPTY_ATTENTION_ART_VARIANTS = 10
1577+ private const val ATTENTION_HEADER_FADE_DISTANCE_PX = 180f
14301578private const val ANALYSIS_POLL_INTERVAL_MS = 1000L
14311579private const val ANALYSIS_TIMEOUT_MS = 30_000L
14321580
0 commit comments