Skip to content

Commit 5c7ac76

Browse files
authored
Merge pull request #3500 from element-hq/feature/fga/pinned_message_icon
Pinned messages : add pin icon in timeline for pinned events.
2 parents a55a98a + 90d7a57 commit 5c7ac76

File tree

178 files changed

+397
-363
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

178 files changed

+397
-363
lines changed

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/list/PinnedMessagesListPresenter.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ class PinnedMessagesListPresenter @AssistedInject constructor(
8282
userHasPermissionToSendMessage = false,
8383
userHasPermissionToSendReaction = false,
8484
isCallOngoing = false,
85+
// don't compute this value or the pin icon will be shown
86+
pinnedEventIds = emptyList()
8587
)
8688
}
8789

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ class TimelinePresenter @AssistedInject constructor(
233233
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
234234
userHasPermissionToSendReaction = userHasPermissionToSendReaction,
235235
isCallOngoing = roomInfo?.hasRoomCall.orFalse(),
236+
pinnedEventIds = roomInfo?.pinnedEventIds.orEmpty(),
236237
)
237238
}
238239
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,5 @@ data class TimelineRoomInfo(
6767
val userHasPermissionToSendMessage: Boolean,
6868
val userHasPermissionToSendReaction: Boolean,
6969
val isCallOngoing: Boolean,
70+
val pinnedEventIds: List<EventId>
7071
)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,12 @@ internal fun aTimelineRoomInfo(
240240
name: String = "Room name",
241241
isDm: Boolean = false,
242242
userHasPermissionToSendMessage: Boolean = true,
243+
pinnedEventIds: List<EventId> = emptyList(),
243244
) = TimelineRoomInfo(
244245
isDm = isDm,
245246
name = name,
246247
userHasPermissionToSendMessage = userHasPermissionToSendMessage,
247248
userHasPermissionToSendReaction = true,
248249
isCallOngoing = false,
250+
pinnedEventIds = pinnedEventIds,
249251
)

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@ fun TimelineView(
128128
Box(modifier) {
129129
LazyColumn(
130130
modifier = Modifier
131-
.fillMaxSize()
132-
.nestedScroll(nestedScrollConnection),
131+
.fillMaxSize()
132+
.nestedScroll(nestedScrollConnection),
133133
state = lazyListState,
134134
reverseLayout = useReverseLayout,
135135
contentPadding = PaddingValues(vertical = 8.dp),
@@ -269,8 +269,8 @@ private fun BoxScope.TimelineScrollHelper(
269269
// Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered
270270
isVisible = !canAutoScroll || forceJumpToBottomVisibility || !isLive,
271271
modifier = Modifier
272-
.align(Alignment.BottomEnd)
273-
.padding(end = 24.dp, bottom = 12.dp),
272+
.align(Alignment.BottomEnd)
273+
.padding(end = 24.dp, bottom = 12.dp),
274274
onClick = { jumpToBottom() },
275275
)
276276
}
@@ -297,8 +297,8 @@ private fun JumpToBottomButton(
297297
) {
298298
Icon(
299299
modifier = Modifier
300-
.size(24.dp)
301-
.rotate(90f),
300+
.size(24.dp)
301+
.rotate(90f),
302302
imageVector = CompoundIcons.ArrowRight(),
303303
contentDescription = stringResource(id = CommonStrings.a11y_jump_to_bottom)
304304
)
@@ -312,12 +312,18 @@ internal fun TimelineViewPreview(
312312
@PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent
313313
) = ElementPreview {
314314
val timelineItems = aTimelineItemList(content)
315+
val timelineEvents = timelineItems.filterIsInstance<TimelineItem.Event>()
316+
val lastEventIdFromMe = timelineEvents.firstOrNull { it.isMine }?.eventId
317+
val lastEventIdFromOther = timelineEvents.firstOrNull { !it.isMine }?.eventId
315318
CompositionLocalProvider(
316319
LocalTimelineItemPresenterFactories provides aFakeTimelineItemPresenterFactories(),
317320
) {
318321
TimelineView(
319322
state = aTimelineState(
320323
timelineItems = timelineItems,
324+
timelineRoomInfo = aTimelineRoomInfo(
325+
pinnedEventIds = listOfNotNull(lastEventIdFromMe, lastEventIdFromOther)
326+
),
321327
focusedEventIndex = 0,
322328
),
323329
typingNotificationState = aTypingNotificationState(),

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

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
1111
import androidx.compose.foundation.combinedClickable
1212
import androidx.compose.foundation.interaction.MutableInteractionSource
1313
import androidx.compose.foundation.layout.Box
14-
import androidx.compose.foundation.layout.fillMaxWidth
15-
import androidx.compose.foundation.layout.offset
14+
import androidx.compose.foundation.layout.BoxWithConstraints
1615
import androidx.compose.foundation.layout.padding
1716
import androidx.compose.foundation.layout.size
1817
import androidx.compose.foundation.layout.widthIn
@@ -40,6 +39,7 @@ import io.element.android.libraries.core.extensions.to01
4039
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
4140
import io.element.android.libraries.designsystem.preview.ElementPreview
4241
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
42+
import io.element.android.libraries.designsystem.text.toDp
4343
import io.element.android.libraries.designsystem.text.toPx
4444
import io.element.android.libraries.designsystem.theme.components.Surface
4545
import io.element.android.libraries.designsystem.theme.components.Text
@@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.TestTags
4949
import io.element.android.libraries.testtags.testTag
5050

5151
private val BUBBLE_RADIUS = 12.dp
52-
internal val BUBBLE_INCOMING_OFFSET = 16.dp
5352
private val avatarRadius = AvatarSize.TimelineSender.dp / 2
5453

55-
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now.
56-
private const val BUBBLE_WIDTH_RATIO = 0.85f
54+
// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 78% now.
55+
private const val BUBBLE_WIDTH_RATIO = 0.78f
56+
private val MIN_BUBBLE_WIDTH = 80.dp
5757

5858
@OptIn(ExperimentalFoundationApi::class)
5959
@Composable
@@ -93,14 +93,6 @@ fun MessageEventBubble(
9393
}
9494
}
9595

96-
fun Modifier.offsetForItem(): Modifier {
97-
return when {
98-
state.isMine -> this
99-
state.timelineRoomInfo.isDm -> this
100-
else -> offset(x = BUBBLE_INCOMING_OFFSET)
101-
}
102-
}
103-
10496
// Ignore state.isHighlighted for now, we need a design decision on it.
10597
val backgroundBubbleColor = when {
10698
state.isMine -> ElementTheme.colors.messageFromMeBackground
@@ -109,11 +101,8 @@ fun MessageEventBubble(
109101
val bubbleShape = bubbleShape()
110102
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
111103
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
112-
Box(
104+
BoxWithConstraints(
113105
modifier = modifier
114-
.fillMaxWidth(BUBBLE_WIDTH_RATIO)
115-
.padding(start = avatarRadius, end = 16.dp)
116-
.offsetForItem()
117106
.graphicsLayer {
118107
compositingStrategy = CompositingStrategy.Offscreen
119108
}
@@ -138,7 +127,10 @@ fun MessageEventBubble(
138127
Surface(
139128
modifier = Modifier
140129
.testTag(TestTags.messageBubble)
141-
.widthIn(min = 80.dp)
130+
.widthIn(
131+
min = MIN_BUBBLE_WIDTH,
132+
max = (constraints.maxWidth * BUBBLE_WIDTH_RATIO).toInt().toDp()
133+
)
142134
.clip(bubbleShape)
143135
.combinedClickable(
144136
onClick = onClick,

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

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.absoluteOffset
2222
import androidx.compose.foundation.layout.fillMaxWidth
2323
import androidx.compose.foundation.layout.height
2424
import androidx.compose.foundation.layout.padding
25+
import androidx.compose.foundation.layout.size
2526
import androidx.compose.foundation.layout.width
2627
import androidx.compose.foundation.layout.wrapContentHeight
2728
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -100,6 +101,8 @@ val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp
100101
// Width of the transparent border around the sender avatar
101102
val SENDER_AVATAR_BORDER_WIDTH = 3.dp
102103

104+
private val BUBBLE_INCOMING_OFFSET = 16.dp
105+
103106
@Composable
104107
fun TimelineItemEventRow(
105108
event: TimelineItem.Event,
@@ -277,6 +280,7 @@ private fun TimelineItemEventRowContent(
277280
sender,
278281
message,
279282
reactions,
283+
pinIcon,
280284
) = createRefs()
281285

282286
// Sender
@@ -311,7 +315,12 @@ private fun TimelineItemEventRowContent(
311315
modifier = Modifier
312316
.constrainAs(message) {
313317
top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE)
314-
this.linkStartOrEnd(event)
318+
if (event.isMine) {
319+
end.linkTo(parent.end, margin = 16.dp)
320+
} else {
321+
val startMargin = if (timelineRoomInfo.isDm) 16.dp else 16.dp + BUBBLE_INCOMING_OFFSET
322+
start.linkTo(parent.start, margin = startMargin)
323+
}
315324
},
316325
state = bubbleState,
317326
interactionSource = interactionSource,
@@ -327,6 +336,27 @@ private fun TimelineItemEventRowContent(
327336
)
328337
}
329338

339+
// Pin icon
340+
val isEventPinned = timelineRoomInfo.pinnedEventIds.contains(event.eventId)
341+
if (isEventPinned) {
342+
Icon(
343+
imageVector = CompoundIcons.PinSolid(),
344+
contentDescription = stringResource(CommonStrings.common_pinned),
345+
tint = ElementTheme.colors.iconTertiary,
346+
modifier = Modifier
347+
.padding(1.dp)
348+
.size(16.dp)
349+
.constrainAs(pinIcon) {
350+
top.linkTo(message.top)
351+
if (event.isMine) {
352+
end.linkTo(message.start, margin = 8.dp)
353+
} else {
354+
start.linkTo(message.end, margin = 8.dp)
355+
}
356+
}
357+
)
358+
}
359+
330360
// Reactions
331361
if (event.reactionsState.reactions.isNotEmpty()) {
332362
TimelineItemReactionsView(
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

0 commit comments

Comments
 (0)