Skip to content

Commit

Permalink
Merge pull request #3500 from element-hq/feature/fga/pinned_message_icon
Browse files Browse the repository at this point in the history
Pinned messages : add pin icon in timeline for pinned events.
  • Loading branch information
ganfra authored Sep 20, 2024
2 parents a55a98a + 90d7a57 commit 5c7ac76
Show file tree
Hide file tree
Showing 178 changed files with 397 additions and 363 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
userHasPermissionToSendMessage = false,
userHasPermissionToSendReaction = false,
isCallOngoing = false,
// don't compute this value or the pin icon will be shown
pinnedEventIds = emptyList()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,7 @@ class TimelinePresenter @AssistedInject constructor(
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,5 @@ data class TimelineRoomInfo(
val userHasPermissionToSendMessage: Boolean,
val userHasPermissionToSendReaction: Boolean,
val isCallOngoing: Boolean,
val pinnedEventIds: List<EventId>
)
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,12 @@ internal fun aTimelineRoomInfo(
name: String = "Room name",
isDm: Boolean = false,
userHasPermissionToSendMessage: Boolean = true,
pinnedEventIds: List<EventId> = emptyList(),
) = TimelineRoomInfo(
isDm = isDm,
name = name,
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
userHasPermissionToSendReaction = true,
isCallOngoing = false,
pinnedEventIds = pinnedEventIds,
)
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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() },
)
}
Expand All @@ -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)
)
Expand All @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
}
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
sender,
message,
reactions,
pinIcon,
) = createRefs()

// Sender
Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 5c7ac76

Please sign in to comment.