Pinned messages : add pin icon in timeline for pinned events.
This commit is contained in:
@@ -128,8 +128,8 @@ fun TimelineView(
|
||||
Box(modifier) {
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
.fillMaxSize()
|
||||
.nestedScroll(nestedScrollConnection),
|
||||
state = lazyListState,
|
||||
reverseLayout = useReverseLayout,
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
@@ -269,8 +269,8 @@ private fun BoxScope.TimelineScrollHelper(
|
||||
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(end = 24.dp, bottom = 12.dp),
|
||||
onClick = { jumpToBottom() },
|
||||
)
|
||||
}
|
||||
@@ -297,8 +297,8 @@ private fun JumpToBottomButton(
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
.size(24.dp)
|
||||
.rotate(90f),
|
||||
imageVector = CompoundIcons.ArrowRight(),
|
||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||
)
|
||||
@@ -312,12 +312,18 @@ internal fun TimelineViewPreview(
|
||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||
) = ElementPreview {
|
||||
val timelineItems = aTimelineItemList(content)
|
||||
val timelineEvents = timelineItems.filterIsInstance<TimelineItem.Event>()
|
||||
val lastEventIdFromMe = timelineEvents.firstOrNull { it.isMine }?.eventId
|
||||
val lastEventIdFromOther = timelineEvents.firstOrNull { !it.isMine }?.eventId
|
||||
CompositionLocalProvider(
|
||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||
) {
|
||||
TimelineView(
|
||||
state = aTimelineState(
|
||||
timelineItems = timelineItems,
|
||||
timelineRoomInfo = aTimelineRoomInfo(
|
||||
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
|
||||
),
|
||||
focusedEventIndex = 0,
|
||||
),
|
||||
typingNotificationState = aTypingNotificationState(),
|
||||
|
||||
@@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
@@ -40,6 +39,7 @@ import io.element.android.libraries.core.extensions.to01
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.text.toDp
|
||||
import io.element.android.libraries.designsystem.text.toPx
|
||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
@@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.testtags.testTag
|
||||
|
||||
private val BUBBLE_RADIUS = 12.dp
|
||||
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
||||
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.85f
|
||||
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
|
||||
private const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||
private val MIN_BUBBLE_WIDTH = 80.dp
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
@@ -93,14 +93,6 @@ fun MessageEventBubble(
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.offsetForItem(): Modifier {
|
||||
return when {
|
||||
state.isMine -> this
|
||||
state.timelineRoomInfo.isDm -> this
|
||||
else -> offset(x = BUBBLE_INCOMING_OFFSET)
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||
val backgroundBubbleColor = when {
|
||||
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
||||
@@ -109,11 +101,8 @@ fun MessageEventBubble(
|
||||
val bubbleShape = bubbleShape()
|
||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||
Box(
|
||||
BoxWithConstraints(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
||||
.padding(start = avatarRadius, end = 16.dp)
|
||||
.offsetForItem()
|
||||
.graphicsLayer {
|
||||
compositingStrategy = CompositingStrategy.Offscreen
|
||||
}
|
||||
@@ -138,7 +127,10 @@ fun MessageEventBubble(
|
||||
Surface(
|
||||
modifier = Modifier
|
||||
.testTag(TestTags.messageBubble)
|
||||
.widthIn(min = 80.dp)
|
||||
.widthIn(
|
||||
min = MIN_BUBBLE_WIDTH,
|
||||
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
||||
)
|
||||
.clip(bubbleShape)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
|
||||
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -100,6 +101,8 @@ val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
|
||||
// Width of the transparent border around the sender avatar
|
||||
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
||||
|
||||
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||
|
||||
@Composable
|
||||
fun TimelineItemEventRow(
|
||||
event: TimelineItem.Event,
|
||||
@@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
|
||||
sender,
|
||||
message,
|
||||
reactions,
|
||||
pinIcon,
|
||||
) = createRefs()
|
||||
|
||||
// Sender
|
||||
@@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
|
||||
modifier = Modifier
|
||||
.constrainAs(message) {
|
||||
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
|
||||
this.linkStartOrEnd(event)
|
||||
if (event.isMine) {
|
||||
end.linkTo(parent.end, margin = 16.dp)
|
||||
} else {
|
||||
val startMargin = if (timelineRoomInfo.isDm) 16.dp else 16.dp + BUBBLE_INCOMING_OFFSET
|
||||
start.linkTo(parent.start, margin = startMargin)
|
||||
}
|
||||
},
|
||||
state = bubbleState,
|
||||
interactionSource = interactionSource,
|
||||
@@ -327,6 +336,27 @@ private fun TimelineItemEventRowContent(
|
||||
)
|
||||
}
|
||||
|
||||
// Pin icon
|
||||
val isEventPinned = timelineRoomInfo.pinnedEventIds.contains(event.eventId)
|
||||
if (isEventPinned) {
|
||||
Icon(
|
||||
imageVector = CompoundIcons.PinSolid(),
|
||||
contentDescription = stringResource(CommonStrings.common_pinned),
|
||||
tint = ElementTheme.colors.iconTertiary,
|
||||
modifier = Modifier
|
||||
.padding(1.dp)
|
||||
.size(16.dp)
|
||||
.constrainAs(pinIcon) {
|
||||
top.linkTo(message.top)
|
||||
if (event.isMine) {
|
||||
end.linkTo(message.start, margin = 8.dp)
|
||||
} else {
|
||||
start.linkTo(message.end, margin = 8.dp)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Reactions
|
||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||
TimelineItemReactionsView(
|
||||
|
||||
Reference in New Issue
Block a user