Use day of week in message list

Closes #4715
This commit is contained in:
Simon Tenbeitel 2020-05-24 12:14:13 +02:00 committed by cketti
parent c32238149d
commit da350055c1
5 changed files with 197 additions and 11 deletions

View file

@ -8,7 +8,6 @@ import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.format.DateUtils
import android.text.style.AbsoluteSizeSpan
import android.text.style.ForegroundColorSpan
import android.text.style.StyleSpan
@ -26,6 +25,7 @@ import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.mail.Address
import com.fsck.k9.ui.R
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import com.fsck.k9.ui.messagelist.MessageListAppearance
import com.fsck.k9.ui.messagelist.MessageListItem
import com.fsck.k9.ui.resolveColorAttribute
@ -39,7 +39,8 @@ class MessageListAdapter internal constructor(
private val layoutInflater: LayoutInflater,
private val contactsPictureLoader: ContactPictureLoader,
private val listItemListener: MessageListItemActionListener,
private val appearance: MessageListAppearance
private val appearance: MessageListAppearance,
private val relativeDateTimeFormatter: RelativeDateTimeFormatter
) : BaseAdapter() {
private val forwardedIcon: Drawable = theme.resolveDrawableAttribute(R.attr.messageListForwarded)
@ -153,7 +154,7 @@ class MessageListAdapter internal constructor(
with(message) {
val maybeBoldTypeface = if (isRead) Typeface.NORMAL else Typeface.BOLD
val displayDate = DateUtils.getRelativeTimeSpanString(context, messageDate)
val displayDate = relativeDateTimeFormatter.formatDate(messageDate)
val displayThreadCount = if (appearance.showingThreadedList) threadCount else 0
val subject = MlfUtils.buildSubject(subject, res.getString(R.string.general_no_subject), displayThreadCount)

View file

@ -40,6 +40,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import com.fsck.k9.Account;
import com.fsck.k9.Account.SortType;
import com.fsck.k9.Clock;
import com.fsck.k9.DI;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
@ -60,6 +61,7 @@ import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.ui.R;
import com.fsck.k9.ui.folders.FolderNameFormatter;
import com.fsck.k9.ui.folders.FolderNameFormatterFactory;
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter;
import com.fsck.k9.ui.messagelist.MessageListAppearance;
import com.fsck.k9.ui.messagelist.MessageListConfig;
import com.fsck.k9.ui.messagelist.MessageListFragmentDiContainer;
@ -475,7 +477,8 @@ public class MessageListFragment extends Fragment implements OnItemClickListener
layoutInflater,
ContactPicture.getContactPictureLoader(),
this,
getMessageListAppearance()
getMessageListAppearance(),
new RelativeDateTimeFormatter(requireContext(), Clock.INSTANCE)
);
if (singleFolderMode) {

View file

@ -0,0 +1,47 @@
package com.fsck.k9.ui.helper
import android.content.Context
import android.text.format.DateUtils
import android.text.format.DateUtils.FORMAT_ABBREV_MONTH
import android.text.format.DateUtils.FORMAT_ABBREV_WEEKDAY
import android.text.format.DateUtils.FORMAT_NUMERIC_DATE
import android.text.format.DateUtils.FORMAT_SHOW_DATE
import android.text.format.DateUtils.FORMAT_SHOW_TIME
import android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY
import android.text.format.DateUtils.FORMAT_SHOW_YEAR
import com.fsck.k9.Clock
import java.util.Calendar
import java.util.Calendar.DAY_OF_WEEK
import java.util.Calendar.YEAR
/**
* Formatter to describe timestamps as a time relative to now.
*/
class RelativeDateTimeFormatter(private val context: Context, private val clock: Clock) {
fun formatDate(timestamp: Long): String {
val now = clock.time.toCalendar()
val date = timestamp.toCalendar()
val format = when {
date.isToday() -> FORMAT_SHOW_TIME
date.isWithinPastSevenDaysOf(now) -> FORMAT_SHOW_WEEKDAY or FORMAT_ABBREV_WEEKDAY
date.isSameYearAs(now) -> FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH
else -> FORMAT_SHOW_DATE or FORMAT_SHOW_YEAR or FORMAT_NUMERIC_DATE
}
return DateUtils.formatDateRange(context, timestamp, timestamp, format)
}
}
private fun Long.toCalendar(): Calendar {
val calendar = Calendar.getInstance()
calendar.timeInMillis = this
return calendar
}
private fun Calendar.isToday() = DateUtils.isToday(this.timeInMillis)
private fun Calendar.isWithinPastSevenDaysOf(other: Calendar) = this.before(other) &&
DateUtils.WEEK_IN_MILLIS > other.timeInMillis - this.timeInMillis &&
this[DAY_OF_WEEK] != other[DAY_OF_WEEK]
private fun Calendar.isSameYearAs(other: Calendar) = this[YEAR] == other[YEAR]

View file

@ -13,6 +13,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.fsck.k9.Account
import com.fsck.k9.Clock
import com.fsck.k9.FontSizes
import com.fsck.k9.FontSizes.FONT_DEFAULT
import com.fsck.k9.FontSizes.LARGE
@ -21,6 +22,7 @@ import com.fsck.k9.contacts.ContactPictureLoader
import com.fsck.k9.mail.Address
import com.fsck.k9.textString
import com.fsck.k9.ui.R
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import com.fsck.k9.ui.messagelist.MessageListAppearance
import com.fsck.k9.ui.messagelist.MessageListItem
import com.nhaarman.mockitokotlin2.mock
@ -455,13 +457,14 @@ class MessageListAdapterTest : RobolectricTest() {
)
return MessageListAdapter(
context = context,
theme = context.theme,
res = context.resources,
layoutInflater = LayoutInflater.from(context),
contactsPictureLoader = contactsPictureLoader,
listItemListener = listItemListener,
appearance = appearance
context = context,
theme = context.theme,
res = context.resources,
layoutInflater = LayoutInflater.from(context),
contactsPictureLoader = contactsPictureLoader,
listItemListener = listItemListener,
appearance = appearance,
relativeDateTimeFormatter = RelativeDateTimeFormatter(context, Clock.INSTANCE)
)
}

View file

@ -0,0 +1,132 @@
package com.fsck.k9.ui.helper
import android.os.SystemClock
import com.fsck.k9.Clock
import com.fsck.k9.RobolectricTest
import com.nhaarman.mockitokotlin2.whenever
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.TimeZone
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
import org.robolectric.RuntimeEnvironment
import org.robolectric.annotation.Config
@Config(qualifiers = "en")
class RelativeDateTimeFormatterTest : RobolectricTest() {
private val context = RuntimeEnvironment.application.applicationContext
private val clock = Mockito.mock(Clock::class.java)
private val dateTimeFormatter = RelativeDateTimeFormatter(context, clock)
private val zoneId = "Europe/Berlin"
@Before
fun setUp() {
TimeZone.setDefault(TimeZone.getTimeZone(zoneId))
}
@Test
fun inFiveMinutesOnNextDay_shouldReturnDay() {
setClockTo("2020-05-17T23:58")
val date = "2020-05-18T00:03".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("May 18", displayDate)
}
@Test
fun oneMinuteAgo_shouldReturnTime() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-17T15:41".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("3:41 PM", displayDate)
}
@Test
fun sixHoursAgo_shouldReturnTime() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-17T09:42".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("9:42 AM", displayDate)
}
@Test
fun yesterday_shouldReturnWeekday() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-16T15:42".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("Sat", displayDate)
}
@Test
fun sixDaysAgo_shouldReturnWeekday() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-11T09:42".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("Mon", displayDate)
}
@Test
fun sixDaysAndTwentyHours_shouldReturnDay() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-10T17:42".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("May 10", displayDate)
}
@Test
fun sevenDaysAndTwoHours_shouldReturnDay() {
setClockTo("2020-05-17T15:42")
val date = "2020-05-10T13:42".toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("May 10", displayDate)
}
@Test
fun startOfYear_shouldReturnDay() {
setClockTo("2020-05-17T15:42")
val date = LocalDate.parse("2020-01-01").atStartOfDay().toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("Jan 1", displayDate)
}
@Test
fun endOfLastYear_shouldReturnDate() {
setClockTo("2020-05-17T15:42")
val date = LocalDateTime.parse("2019-12-31T23:59").toEpochMillis()
val displayDate = dateTimeFormatter.formatDate(date)
assertEquals("12/31/2019", displayDate)
}
private fun setClockTo(time: String) {
val dateTime = LocalDateTime.parse(time)
val timeInMillis = dateTime.toEpochMillis()
SystemClock.setCurrentTimeMillis(timeInMillis) // Is handled by ShadowSystemClock
whenever(clock.time).thenReturn(timeInMillis)
}
private fun String.toEpochMillis() = LocalDateTime.parse(this).toEpochMillis()
private fun LocalDateTime.toEpochMillis() = this.atZone(ZoneId.of(zoneId)).toInstant().toEpochMilli()
}