Skip to content

Commit d5b3eea

Browse files
authored
Merge pull request #4026 from element-hq/feature/bma/monthSeparators
Implement month separator for the Gallery, and improve date rendering.
2 parents a665517 + a99eecd commit d5b3eea

File tree

100 files changed

+1703
-402
lines changed

Some content is hidden

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

100 files changed

+1703
-402
lines changed

.maestro/tests/roomList/createAndDeleteRoom.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,6 @@ appId: ${MAESTRO_APP_ID}
3030
# assert there's 1 member and 2 invitees
3131
- tapOn: "Back"
3232
- scroll
33+
- scroll
3334
- tapOn: "Leave room"
3435
- tapOn: "Leave"

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ import io.element.android.libraries.architecture.createNode
5555
import io.element.android.libraries.architecture.overlay.Overlay
5656
import io.element.android.libraries.architecture.overlay.operation.hide
5757
import io.element.android.libraries.architecture.overlay.operation.show
58+
import io.element.android.libraries.dateformatter.api.DateFormatter
59+
import io.element.android.libraries.dateformatter.api.DateFormatterMode
5860
import io.element.android.libraries.di.RoomScope
5961
import io.element.android.libraries.matrix.api.MatrixClient
6062
import io.element.android.libraries.matrix.api.core.EventId
@@ -97,6 +99,7 @@ class MessagesFlowNode @AssistedInject constructor(
9799
private val pinnedEventsTimelineProvider: PinnedEventsTimelineProvider,
98100
private val timelineController: TimelineController,
99101
private val knockRequestsListEntryPoint: KnockRequestsListEntryPoint,
102+
private val dateFormatter: DateFormatter,
100103
) : BaseFlowNode<MessagesFlowNode.NavTarget>(
101104
backstack = BackStack(
102105
initialElement = plugins.filterIsInstance<MessagesEntryPoint.Params>().first().initialTarget.toNavTarget(),
@@ -436,7 +439,14 @@ class MessagesFlowNode @AssistedInject constructor(
436439
senderId = event.senderId,
437440
senderName = event.safeSenderName,
438441
senderAvatar = event.senderAvatar.url,
439-
dateSent = event.sentTime,
442+
dateSent = dateFormatter.format(
443+
event.sentTimeMillis,
444+
mode = DateFormatterMode.Day,
445+
),
446+
dateSentFull = dateFormatter.format(
447+
timestamp = event.sentTimeMillis,
448+
mode = DateFormatterMode.Full,
449+
),
440450
),
441451
mediaSource = mediaSource,
442452
thumbnailSource = thumbnailSource,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.canBeCopie
3737
import io.element.android.features.messages.impl.timeline.model.event.canBeForwarded
3838
import io.element.android.features.messages.impl.timeline.model.event.canReact
3939
import io.element.android.libraries.architecture.Presenter
40+
import io.element.android.libraries.dateformatter.api.DateFormatter
41+
import io.element.android.libraries.dateformatter.api.DateFormatterMode
4042
import io.element.android.libraries.di.RoomScope
4143
import io.element.android.libraries.featureflag.api.FeatureFlagService
4244
import io.element.android.libraries.featureflag.api.FeatureFlags
@@ -64,6 +66,7 @@ class DefaultActionListPresenter @AssistedInject constructor(
6466
private val room: MatrixRoom,
6567
private val userSendFailureFactory: VerifiedUserSendFailureFactory,
6668
private val featureFlagService: FeatureFlagService,
69+
private val dateFormatter: DateFormatter,
6770
) : ActionListPresenter {
6871
@AssistedFactory
6972
@ContributesBinding(RoomScope::class)
@@ -131,6 +134,11 @@ class DefaultActionListPresenter @AssistedInject constructor(
131134
if (actions.isNotEmpty() || displayEmojiReactions || verifiedUserSendFailure != VerifiedUserSendFailure.None) {
132135
target.value = ActionListState.Target.Success(
133136
event = timelineItem,
137+
sentTimeFull = dateFormatter.format(
138+
timelineItem.sentTimeMillis,
139+
DateFormatterMode.Full,
140+
useRelative = true,
141+
),
134142
displayEmojiReactions = displayEmojiReactions,
135143
verifiedUserSendFailure = verifiedUserSendFailure,
136144
actions = actions.toImmutableList()

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ data class ActionListState(
2424
data class Loading(val event: TimelineItem.Event) : Target
2525
data class Success(
2626
val event: TimelineItem.Event,
27+
val sentTimeFull: String,
2728
val displayEmojiReactions: Boolean,
2829
val verifiedUserSendFailure: VerifiedUserSendFailure,
2930
val actions: ImmutableList<TimelineItemAction>,

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListStateProvider.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
3737
event = aTimelineItemEvent(
3838
timelineItemReactions = reactionsState
3939
),
40+
sentTimeFull = "January 1, 1970 at 12:00 AM",
4041
displayEmojiReactions = true,
4142
verifiedUserSendFailure = VerifiedUserSendFailure.None,
4243
actions = aTimelineItemActionList(),
@@ -49,6 +50,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
4950
displayNameAmbiguous = true,
5051
timelineItemReactions = reactionsState,
5152
),
53+
sentTimeFull = "January 1, 1970 at 12:00 AM",
5254
displayEmojiReactions = true,
5355
verifiedUserSendFailure = VerifiedUserSendFailure.None,
5456
actions = aTimelineItemActionList(
@@ -62,6 +64,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
6264
content = aTimelineItemVideoContent(),
6365
timelineItemReactions = reactionsState
6466
),
67+
sentTimeFull = "January 1, 1970 at 12:00 AM",
6568
displayEmojiReactions = true,
6669
verifiedUserSendFailure = VerifiedUserSendFailure.None,
6770
actions = aTimelineItemActionList(
@@ -75,6 +78,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
7578
content = aTimelineItemFileContent(),
7679
timelineItemReactions = reactionsState
7780
),
81+
sentTimeFull = "January 1, 1970 at 12:00 AM",
7882
displayEmojiReactions = true,
7983
verifiedUserSendFailure = VerifiedUserSendFailure.None,
8084
actions = aTimelineItemActionList(
@@ -88,6 +92,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
8892
content = aTimelineItemAudioContent(),
8993
timelineItemReactions = reactionsState
9094
),
95+
sentTimeFull = "January 1, 1970 at 12:00 AM",
9196
displayEmojiReactions = true,
9297
verifiedUserSendFailure = VerifiedUserSendFailure.None,
9398
actions = aTimelineItemActionList(
@@ -101,6 +106,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
101106
content = aTimelineItemVoiceContent(caption = null),
102107
timelineItemReactions = reactionsState
103108
),
109+
sentTimeFull = "January 1, 1970 at 12:00 AM",
104110
displayEmojiReactions = true,
105111
verifiedUserSendFailure = VerifiedUserSendFailure.None,
106112
actions = aTimelineItemActionList(
@@ -114,6 +120,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
114120
content = aTimelineItemLocationContent(),
115121
timelineItemReactions = reactionsState
116122
),
123+
sentTimeFull = "January 1, 1970 at 12:00 AM",
117124
displayEmojiReactions = true,
118125
verifiedUserSendFailure = VerifiedUserSendFailure.None,
119126
actions = aTimelineItemActionList(),
@@ -125,6 +132,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
125132
content = aTimelineItemLocationContent(),
126133
timelineItemReactions = reactionsState
127134
),
135+
sentTimeFull = "January 1, 1970 at 12:00 AM",
128136
displayEmojiReactions = false,
129137
verifiedUserSendFailure = VerifiedUserSendFailure.None,
130138
actions = aTimelineItemActionList(),
@@ -136,6 +144,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
136144
content = aTimelineItemPollContent(),
137145
timelineItemReactions = reactionsState
138146
),
147+
sentTimeFull = "January 1, 1970 at 12:00 AM",
139148
displayEmojiReactions = false,
140149
verifiedUserSendFailure = VerifiedUserSendFailure.None,
141150
actions = aTimelineItemPollActionList(),
@@ -147,6 +156,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
147156
timelineItemReactions = reactionsState,
148157
messageShield = MessageShield.UnknownDevice(isCritical = true)
149158
),
159+
sentTimeFull = "January 1, 1970 at 12:00 AM",
150160
displayEmojiReactions = true,
151161
verifiedUserSendFailure = VerifiedUserSendFailure.None,
152162
actions = aTimelineItemActionList(),
@@ -155,6 +165,7 @@ open class ActionListStateProvider : PreviewParameterProvider<ActionListState> {
155165
anActionListState(
156166
target = ActionListState.Target.Success(
157167
event = aTimelineItemEvent(),
168+
sentTimeFull = "January 1, 1970 at 12:00 AM",
158169
displayEmojiReactions = true,
159170
verifiedUserSendFailure = anUnsignedDeviceSendFailure(),
160171
actions = aTimelineItemActionList(),

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ private fun ActionListViewContent(
185185
Column {
186186
MessageSummary(
187187
event = target.event,
188+
sentTimeFull = target.sentTimeFull,
188189
modifier = Modifier
189190
.fillMaxWidth()
190191
.padding(horizontal = 16.dp)
@@ -245,7 +246,11 @@ private fun ActionListViewContent(
245246

246247
@Suppress("MultipleEmitters") // False positive
247248
@Composable
248-
private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) {
249+
private fun MessageSummary(
250+
event: TimelineItem.Event,
251+
sentTimeFull: String,
252+
modifier: Modifier = Modifier,
253+
) {
249254
val content: @Composable () -> Unit
250255
val icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.MessageActionSender)) }
251256
val contentStyle = ElementTheme.typography.fontBodyMdRegular.copy(color = MaterialTheme.colorScheme.secondary)
@@ -300,20 +305,23 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif
300305
icon()
301306
Spacer(modifier = Modifier.width(8.dp))
302307
Column(modifier = Modifier.weight(1f)) {
303-
SenderName(
304-
senderId = event.senderId,
305-
senderProfile = event.senderProfile,
306-
senderNameMode = SenderNameMode.ActionList,
307-
)
308+
Row {
309+
SenderName(
310+
modifier = Modifier.weight(1f),
311+
senderId = event.senderId,
312+
senderProfile = event.senderProfile,
313+
senderNameMode = SenderNameMode.ActionList,
314+
)
315+
Spacer(modifier = Modifier.width(16.dp))
316+
Text(
317+
text = sentTimeFull,
318+
style = ElementTheme.typography.fontBodyXsRegular,
319+
color = MaterialTheme.colorScheme.secondary,
320+
textAlign = TextAlign.End,
321+
)
322+
}
308323
content()
309324
}
310-
Spacer(modifier = Modifier.width(16.dp))
311-
Text(
312-
event.sentTime,
313-
style = ElementTheme.typography.fontBodyXsRegular,
314-
color = MaterialTheme.colorScheme.secondary,
315-
textAlign = TextAlign.End,
316-
)
317325
}
318326
}
319327

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou
2020
import io.element.android.features.messages.impl.timeline.model.TimelineItemReactions
2121
import io.element.android.features.messages.impl.timeline.model.TimelineItemReadReceipts
2222
import io.element.android.libraries.core.bool.orTrue
23-
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
23+
import io.element.android.libraries.dateformatter.api.DateFormatter
24+
import io.element.android.libraries.dateformatter.api.DateFormatterMode
2425
import io.element.android.libraries.designsystem.components.avatar.AvatarData
2526
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
2627
import io.element.android.libraries.matrix.api.MatrixClient
@@ -32,14 +33,13 @@ import io.element.android.libraries.matrix.api.timeline.item.event.getDisambigua
3233
import io.element.android.libraries.matrix.ui.messages.reply.map
3334
import kotlinx.collections.immutable.persistentListOf
3435
import kotlinx.collections.immutable.toImmutableList
35-
import java.text.DateFormat
3636
import java.util.Date
3737

3838
class TimelineItemEventFactory @AssistedInject constructor(
3939
@Assisted private val config: TimelineItemsFactoryConfig,
4040
private val contentFactory: TimelineItemContentFactory,
4141
private val matrixClient: MatrixClient,
42-
private val lastMessageTimestampFormatter: LastMessageTimestampFormatter,
42+
private val dateFormatter: DateFormatter,
4343
private val permalinkParser: PermalinkParser,
4444
) {
4545
@AssistedFactory
@@ -57,9 +57,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
5757
val groupPosition =
5858
computeGroupPosition(currentTimelineItem, timelineItems, index)
5959
val senderProfile = currentTimelineItem.event.senderProfile
60-
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
61-
val sentTime = timeFormatter.format(Date(currentTimelineItem.event.timestamp))
62-
60+
val sentTime = dateFormatter.format(
61+
timestamp = currentTimelineItem.event.timestamp,
62+
mode = DateFormatterMode.TimeOnly,
63+
)
6364
val senderAvatarData = AvatarData(
6465
id = currentSender.value,
6566
name = senderProfile.getDisambiguatedDisplayName(currentSender),
@@ -78,6 +79,7 @@ class TimelineItemEventFactory @AssistedInject constructor(
7879
isMine = currentTimelineItem.event.isOwn,
7980
isEditable = currentTimelineItem.event.isEditable,
8081
canBeRepliedTo = currentTimelineItem.event.canBeRepliedTo,
82+
sentTimeMillis = currentTimelineItem.event.timestamp,
8183
sentTime = sentTime,
8284
groupPosition = groupPosition,
8385
reactionsState = currentTimelineItem.computeReactionsState(),
@@ -106,7 +108,6 @@ class TimelineItemEventFactory @AssistedInject constructor(
106108
if (!config.computeReactions) {
107109
return TimelineItemReactions(reactions = persistentListOf())
108110
}
109-
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
110111
var aggregatedReactions = this.event.reactions.map { reaction ->
111112
// Sort reactions within an aggregation by timestamp descending.
112113
// This puts the most recent at the top, useful in cases like the
@@ -121,7 +122,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
121122
AggregatedReactionSender(
122123
senderId = it.senderId,
123124
timestamp = date,
124-
sentTime = timeFormatter.format(date),
125+
sentTime = dateFormatter.format(
126+
it.timestamp,
127+
DateFormatterMode.TimeOrDate,
128+
),
125129
)
126130
}
127131
.toImmutableList()
@@ -157,7 +161,10 @@ class TimelineItemEventFactory @AssistedInject constructor(
157161
url = roomMember?.avatarUrl,
158162
size = AvatarSize.TimelineReadReceipt,
159163
),
160-
formattedDate = lastMessageTimestampFormatter.format(receipt.timestamp)
164+
formattedDate = dateFormatter.format(
165+
receipt.timestamp,
166+
mode = DateFormatterMode.TimeOrDate,
167+
)
161168
)
162169
}
163170
.toImmutableList()

features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemDaySeparatorFactory.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ package io.element.android.features.messages.impl.timeline.factories.virtual
99

1010
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel
1111
import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel
12-
import io.element.android.libraries.dateformatter.api.DaySeparatorFormatter
12+
import io.element.android.libraries.dateformatter.api.DateFormatter
13+
import io.element.android.libraries.dateformatter.api.DateFormatterMode
1314
import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem
1415
import javax.inject.Inject
1516

16-
class TimelineItemDaySeparatorFactory @Inject constructor(private val daySeparatorFormatter: DaySeparatorFormatter) {
17+
class TimelineItemDaySeparatorFactory @Inject constructor(
18+
private val dateFormatter: DateFormatter,
19+
) {
1720
fun create(virtualItem: VirtualTimelineItem.DayDivider): TimelineItemVirtualModel {
18-
val formattedDate = daySeparatorFormatter.format(virtualItem.timestamp)
21+
val formattedDate = dateFormatter.format(
22+
timestamp = virtualItem.timestamp,
23+
mode = DateFormatterMode.Day,
24+
useRelative = true,
25+
)
1926
return TimelineItemDaySeparatorModel(
2027
formattedDate = formattedDate
2128
)

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ sealed interface TimelineItem {
7171
val senderProfile: ProfileTimelineDetails,
7272
val senderAvatar: AvatarData,
7373
val content: TimelineItemEventContent,
74+
val sentTimeMillis: Long = 0L,
7475
val sentTime: String = "",
7576
val isMine: Boolean = false,
7677
val isEditable: Boolean,

features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ class MessagesViewTest {
327327
actionListState = anActionListState(
328328
target = ActionListState.Target.Success(
329329
event = timelineItem,
330+
sentTimeFull = "",
330331
displayEmojiReactions = true,
331332
actions = persistentListOf(TimelineItemAction.Edit),
332333
verifiedUserSendFailure = VerifiedUserSendFailure.None,
@@ -399,6 +400,7 @@ class MessagesViewTest {
399400
actionListState = anActionListState(
400401
target = ActionListState.Target.Success(
401402
event = timelineItem,
403+
sentTimeFull = "",
402404
displayEmojiReactions = true,
403405
verifiedUserSendFailure = VerifiedUserSendFailure.None,
404406
actions = persistentListOf(TimelineItemAction.Edit),
@@ -427,6 +429,7 @@ class MessagesViewTest {
427429
actionListState = anActionListState(
428430
target = ActionListState.Target.Success(
429431
event = timelineItem,
432+
sentTimeFull = "",
430433
displayEmojiReactions = true,
431434
verifiedUserSendFailure = aChangedIdentitySendFailure(),
432435
actions = persistentListOf(),

0 commit comments

Comments
 (0)