parent
c32238149d
commit
da350055c1
5 changed files with 197 additions and 11 deletions
|
@ -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)
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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]
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
Loading…
Reference in a new issue