Pinned messages : add pin icon in timeline for pinned events.
This commit is contained in:
parent
6111020cf0
commit
2b796b2dda
3 changed files with 53 additions and 25 deletions
|
|
@ -128,8 +128,8 @@ fun TimelineView(
|
||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(nestedScrollConnection),
|
.nestedScroll(nestedScrollConnection),
|
||||||
state = lazyListState,
|
state = lazyListState,
|
||||||
reverseLayout = useReverseLayout,
|
reverseLayout = useReverseLayout,
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
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
|
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
|
||||||
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
.padding(end = 24.dp, bottom = 12.dp),
|
.padding(end = 24.dp, bottom = 12.dp),
|
||||||
onClick = { jumpToBottom() },
|
onClick = { jumpToBottom() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -297,8 +297,8 @@ private fun JumpToBottomButton(
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(24.dp)
|
.size(24.dp)
|
||||||
.rotate(90f),
|
.rotate(90f),
|
||||||
imageVector = CompoundIcons.ArrowRight(),
|
imageVector = CompoundIcons.ArrowRight(),
|
||||||
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
|
||||||
)
|
)
|
||||||
|
|
@ -312,12 +312,18 @@ internal fun TimelineViewPreview(
|
||||||
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
|
||||||
) = ElementPreview {
|
) = ElementPreview {
|
||||||
val timelineItems = aTimelineItemList(content)
|
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(
|
CompositionLocalProvider(
|
||||||
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
|
||||||
) {
|
) {
|
||||||
TimelineView(
|
TimelineView(
|
||||||
state = aTimelineState(
|
state = aTimelineState(
|
||||||
timelineItems = timelineItems,
|
timelineItems = timelineItems,
|
||||||
|
timelineRoomInfo = aTimelineRoomInfo(
|
||||||
|
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
|
||||||
|
),
|
||||||
focusedEventIndex = 0,
|
focusedEventIndex = 0,
|
||||||
),
|
),
|
||||||
typingNotificationState = aTypingNotificationState(),
|
typingNotificationState = aTypingNotificationState(),
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
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.components.avatar.AvatarSize
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
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.text.toPx
|
||||||
import io.element.android.libraries.designsystem.theme.components.Surface
|
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.components.Text
|
||||||
|
|
@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
|
|
||||||
private val BUBBLE_RADIUS = 12.dp
|
private val BUBBLE_RADIUS = 12.dp
|
||||||
internal val BUBBLE_INCOMING_OFFSET = 16.dp
|
|
||||||
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
|
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.
|
// 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.85f
|
private const val BUBBLE_WIDTH_RATIO = 0.78f
|
||||||
|
private val MIN_BUBBLE_WIDTH = 80.dp
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@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.
|
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||||
val backgroundBubbleColor = when {
|
val backgroundBubbleColor = when {
|
||||||
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
state.isMine -> ElementTheme.colors.messageFromMeBackground
|
||||||
|
|
@ -109,11 +101,8 @@ fun MessageEventBubble(
|
||||||
val bubbleShape = bubbleShape()
|
val bubbleShape = bubbleShape()
|
||||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||||
Box(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
|
|
||||||
.padding(start = avatarRadius, end = 16.dp)
|
|
||||||
.offsetForItem()
|
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
compositingStrategy = CompositingStrategy.Offscreen
|
||||||
}
|
}
|
||||||
|
|
@ -138,7 +127,10 @@ fun MessageEventBubble(
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.testTag(TestTags.messageBubble)
|
.testTag(TestTags.messageBubble)
|
||||||
.widthIn(min = 80.dp)
|
.widthIn(
|
||||||
|
min = MIN_BUBBLE_WIDTH,
|
||||||
|
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
|
||||||
|
)
|
||||||
.clip(bubbleShape)
|
.clip(bubbleShape)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
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
|
// Width of the transparent border around the sender avatar
|
||||||
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
|
||||||
|
|
||||||
|
private val BUBBLE_INCOMING_OFFSET = 16.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TimelineItemEventRow(
|
fun TimelineItemEventRow(
|
||||||
event: TimelineItem.Event,
|
event: TimelineItem.Event,
|
||||||
|
|
@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
|
||||||
sender,
|
sender,
|
||||||
message,
|
message,
|
||||||
reactions,
|
reactions,
|
||||||
|
pinIcon,
|
||||||
) = createRefs()
|
) = createRefs()
|
||||||
|
|
||||||
// Sender
|
// Sender
|
||||||
|
|
@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.constrainAs(message) {
|
.constrainAs(message) {
|
||||||
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
|
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,
|
state = bubbleState,
|
||||||
interactionSource = interactionSource,
|
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
|
// Reactions
|
||||||
if (event.reactionsState.reactions.isNotEmpty()) {
|
if (event.reactionsState.reactions.isNotEmpty()) {
|
||||||
TimelineItemReactionsView(
|
TimelineItemReactionsView(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue