Start reworking date formatting

This commit is contained in:
ganfra 2020-08-28 20:01:10 +02:00 committed by Benoit Marty
parent bf0b6d738a
commit 73ab32fd92
16 changed files with 243 additions and 36 deletions

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class AbbrevDateFormatterProvider @Inject constructor(private val localeProvider: LocaleProvider) : DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofPattern("d MMM", localeProvider.current())
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofPattern("dd.MM.yyyy",localeProvider.current())
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import org.threeten.bp.format.DateTimeFormatter
interface DateFormatterProvider {
val dateWithMonthFormatter: DateTimeFormatter
val dateWithYearFormatter: DateTimeFormatter
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import javax.inject.Inject
class DateFormatterProviders @Inject constructor(private val defaultDateFormatterProvider: DefaultDateFormatterProvider,
private val abbrevDateFormatterProvider: AbbrevDateFormatterProvider) {
fun provide(abbrev: Boolean): DateFormatterProvider {
return if (abbrev) {
abbrevDateFormatterProvider
} else {
defaultDateFormatterProvider
}
}
}

View File

@ -0,0 +1,37 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.content.Context
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class DefaultDateFormatterProvider @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider)
: DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMMMM"))
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMM y"))
}
}

View File

@ -19,8 +19,11 @@ package im.vector.app.core.date
import android.content.Context
import android.text.format.DateFormat
import android.text.format.DateUtils
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.toTimestamp
import org.threeten.bp.LocalDateTime
import org.threeten.bp.Period
import org.threeten.bp.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
@ -41,22 +44,77 @@ fun startOfDay(time: Long): Long {
}
class VectorDateFormatter @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider) {
private val localeProvider: LocaleProvider,
private val dateFormatterProviders: DateFormatterProviders) {
private val messageHourFormatter by lazy {
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
private val hourFormatter by lazy {
if (DateFormat.is24HourFormat(context)) {
DateTimeFormatter.ofPattern("H:mm", localeProvider.current())
} else {
DateTimeFormatter.ofPattern("h:mm a", localeProvider.current())
}
}
private val messageDayFormatter by lazy {
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE d MMM"))
private val dayFormatter by lazy {
DateTimeFormatter.ofPattern("EEE", localeProvider.current())
}
private val fullDateFormatter by lazy {
if (DateFormat.is24HourFormat(context)) {
DateTimeFormatter.ofPattern("EEE, d MMM yyyy H:mm", localeProvider.current())
} else {
DateTimeFormatter.ofPattern("EEE, d MMM yyyy h:mm a", localeProvider.current())
}
}
fun formatMessageHour(localDateTime: LocalDateTime): String {
return messageHourFormatter.format(localDateTime)
return hourFormatter.format(localDateTime)
}
fun formatMessageDay(localDateTime: LocalDateTime): String {
return messageDayFormatter.format(localDateTime)
return dayFormatter.format(localDateTime)
}
fun formatMessageDayWithMonth(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithMonthFormatter.format(localDateTime)
}
fun formatMessageDayWithYear(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithYearFormatter.format(localDateTime)
}
fun formatMessageDate(
date: LocalDateTime?,
showFullDate: Boolean = false,
onlyTimeIfSameDay: Boolean = false,
useRelative: Boolean = false,
alwaysShowYear: Boolean = false,
abbrev: Boolean = false
): String {
if (date == null) {
return ""
}
if (showFullDate) {
return fullDateFormatter.format(date)
}
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
return if (onlyTimeIfSameDay && isSameDay) {
formatMessageHour(date)
} else {
val period = Period.between(date.toLocalDate(), currentDate.toLocalDate())
if (period.years >= 1 || alwaysShowYear) {
formatMessageDayWithYear(date, abbrev)
} else if (period.months >= 1) {
formatMessageDayWithMonth(date, abbrev)
} else if (useRelative && period.days < 2) {
getRelativeDay(date.toTimestamp())
} else if (useRelative && period.days < 7) {
formatMessageDay(date)
} else {
formatMessageDayWithMonth(date, abbrev)
}
}
}
/**
@ -71,12 +129,22 @@ class VectorDateFormatter @Inject constructor(private val context: Context,
return ""
}
val now = System.currentTimeMillis()
var flags = DateUtils.FORMAT_SHOW_WEEKDAY
return DateUtils.getRelativeDateTimeString(
context,
time,
DateUtils.DAY_IN_MILLIS,
now - startOfDay(now - 2 * DateUtils.DAY_IN_MILLIS),
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
flags
).toString()
}
private fun getRelativeDay(ts: Long): String {
return DateUtils.getRelativeTimeSpanString(
ts,
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_SHOW_WEEKDAY).toString()
}
}

View File

@ -19,10 +19,12 @@ package im.vector.app.core.resources
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
import org.threeten.bp.ZoneOffset
object DateProvider {
private val zoneId = ZoneId.systemDefault()
private val zoneOffset = ZoneOffset.UTC
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0)
@ -33,4 +35,11 @@ object DateProvider {
val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId)
}
fun toTimestamp(localDateTime: LocalDateTime): Long {
return localDateTime.toInstant(zoneOffset).toEpochMilli()
}
}
fun LocalDateTime.toTimestamp(): Long = DateProvider.toTimestamp(this)

View File

@ -29,6 +29,7 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.resources.DateProvider
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState
@ -53,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoCon
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime
import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
@ -333,13 +333,16 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
) {
requestModelBuild()
}
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date)
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
}
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? {
private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
return if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date)
val formattedDay = dateFormatter.formatMessageDate(
date = DateProvider.toLocalDateTime(originServerTs),
alwaysShowYear = true
)
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
} else {
null

View File

@ -23,6 +23,7 @@ import im.vector.app.core.extensions.canReact
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.text.SimpleDateFormat
import java.time.LocalDateTime
import java.util.Date
import java.util.Locale
@ -56,11 +57,7 @@ data class MessageActionState(
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
}

View File

@ -20,12 +20,14 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success
import im.vector.app.EmojiCompatFontProvider
import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem
import im.vector.app.core.epoxy.dividerItem
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
@ -40,13 +42,16 @@ import javax.inject.Inject
class MessageActionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider
private val fontProvider: EmojiCompatFontProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() {
var listener: MessageActionsEpoxyControllerListener? = null
override fun buildModels(state: MessageActionState) {
// Message preview
val date = state.timelineEvent()?.root?.localDateTime()
val formattedDate = dateFormatter.formatMessageDate(date, showFullDate = true)
bottomSheetMessagePreviewItem {
id("preview")
avatarRenderer(avatarRenderer)
@ -54,7 +59,7 @@ class MessageActionsEpoxyController @Inject constructor(
movementMethod(createLinkMovementMethod(listener))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(state.messageBody.linkify(listener))
time(state.time())
time(formattedDate)
}
// Send state

View File

@ -83,7 +83,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
// need to display header with day
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
else dateFormatter.formatMessageDay(timelineEvent.localDateTime())
else dateFormatter.formatMessageDayWithMonth(timelineEvent.localDateTime())
genericItemHeader {
id(evDate.hashCode())
text(dateString)

View File

@ -29,12 +29,12 @@ import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import io.reactivex.Observable
import io.reactivex.Single
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.rx.RxRoom
import org.matrix.android.sdk.rx.unwrap
import io.reactivex.Observable
import io.reactivex.Single
data class DisplayReactionsViewState(
val eventId: String,

View File

@ -21,7 +21,6 @@ import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer
@ -53,8 +52,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
}
private fun createInvitationItem(roomSummary: RoomSummary,
changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId
} else {
@ -87,15 +86,13 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestPreviewableEvent
if (latestEvent != null) {
val date = latestEvent.root.localDateTime()
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
latestEventTime = if (isSameDay) {
dateFormatter.formatMessageHour(date)
} else {
dateFormatter.formatMessageDay(date)
}
latestEventTime = dateFormatter.formatMessageDate(
date = latestEvent.root.localDateTime(),
useRelative = true,
onlyTimeIfSameDay = true,
abbrev = true
)
}
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
return RoomSummaryItem_()

View File

@ -79,7 +79,7 @@ class DataAttachmentRoomProvider(
val timeLineEvent = room?.getTimeLineEvent(item.eventId)
if (timeLineEvent != null) {
val dateString = timeLineEvent.root.localDateTime().let {
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
"${dateFormatter.formatMessageDayWithMonth(it)} at ${dateFormatter.formatMessageHour(it)} "
}
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()

View File

@ -129,7 +129,7 @@ class RoomEventsAttachmentProvider(
super.overlayViewAtPosition(context, position)
val item = attachments[position]
val dateString = item.root.localDateTime().let {
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
"${dateFormatter.formatMessageDayWithMonth(it)} at ${dateFormatter.formatMessageHour(it)} "
}
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()

View File

@ -18,12 +18,12 @@ package im.vector.app.features.roomprofile.uploads.files
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState
import org.matrix.android.sdk.api.session.room.uploads.UploadEvent
import im.vector.app.R
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState
import org.matrix.android.sdk.api.session.room.uploads.UploadEvent
import javax.inject.Inject
class UploadsFileController @Inject constructor(

View File

@ -94,7 +94,7 @@ class GossipingEventsEpoxyController @Inject constructor(
)
description(
span {
+vectorDateFormatter.formatMessageDay(DateProvider.toLocalDateTime(event.ageLocalTs))
+vectorDateFormatter.formatMessageDayWithMonth(DateProvider.toLocalDateTime(event.ageLocalTs))
+" ${vectorDateFormatter.formatMessageHour(DateProvider.toLocalDateTime(event.ageLocalTs))}"
span("\nfrom: ") {
textStyle = "bold"