Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,58 @@ 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 java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Calendar.DAY_OF_WEEK
import java.util.Calendar.YEAR
import java.util.Locale
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.daysUntil
import kotlinx.datetime.format.char
import kotlinx.datetime.toLocalDateTime
import net.thunderbird.core.preference.display.visualSettings.message.list.MessageListDateTimeFormat

/**
* Formatter to describe timestamps as a time relative to now.
*
* @param context The context to use for formatting dates.
* @param clock The clock to use for getting the current time.
*/
class RelativeDateTimeFormatter
@OptIn(ExperimentalTime::class)
constructor(
class RelativeDateTimeFormatter(
private val context: Context,
private val clock: Clock,
) {
/**
* Format a date using the given [dateTimeFormat].
*
* @param timestamp The timestamp to format.
* @param dateTimeFormat The date format to use.
*/
fun formatDate(
timestamp: Long,
dateTimeFormat: MessageListDateTimeFormat,
): String {
val timeZone = TimeZone.currentSystemDefault()
val now = clock.now().toLocalDateTime(timeZone)

return formatDate(
timestamp = timestamp,
dateTimeFormat = dateTimeFormat,
now = now,
timeZone = timeZone,
)
}

private fun formatDate(
timestamp: Long,
dateTimeFormat: MessageListDateTimeFormat,
now: LocalDateTime,
timeZone: TimeZone,
): String {
val date = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(timeZone)

fun formatDate(timestamp: Long, dateTimeFormat: MessageListDateTimeFormat): String {
@OptIn(ExperimentalTime::class)
val now = clock.now().toCalendar()
val date = timestamp.toCalendar()
return when (dateTimeFormat) {
MessageListDateTimeFormat.Contextual -> {
val flags = when {
date.isToday() -> FORMAT_SHOW_TIME
date.isSameDayAs(now) -> 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
Expand All @@ -48,30 +72,37 @@ constructor(
DateUtils.formatDateRange(context, timestamp, timestamp, flags)
}
MessageListDateTimeFormat.ISO -> {
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
sdf.format(java.util.Date(timestamp))
isoDateTimeFormat.format(date)
}
}
}
}

private fun Long.toCalendar(): Calendar {
val calendar = Calendar.getInstance()
calendar.timeInMillis = this
return calendar
}
private fun LocalDateTime.isSameDayAs(other: LocalDateTime): Boolean {
return date == other.date
}

@OptIn(ExperimentalTime::class)
private fun Instant.toCalendar(): Calendar {
val calendar = Calendar.getInstance()
calendar.timeInMillis = this.toEpochMilliseconds()
return calendar
}
private fun LocalDateTime.isWithinPastSevenDaysOf(other: LocalDateTime): Boolean {
val daysUntil = date.daysUntil(other.date)
return daysUntil in 1 until DAYS_PER_WEEK
}

private fun Calendar.isToday() = DateUtils.isToday(this.timeInMillis)
private fun LocalDateTime.isSameYearAs(other: LocalDateTime): Boolean {
return year == other.year
}

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 companion object {
private const val DAYS_PER_WEEK = 7

private fun Calendar.isSameYearAs(other: Calendar) = this[YEAR] == other[YEAR]
val isoDateTimeFormat = LocalDateTime.Format {
year()
char('-')
monthNumber()
char('-')
day()
char(' ')
hour()
char(':')
minute()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ val messageListUiModule = module {
messageHelper = get(),
messageListPreferencesManager = get(),
outboxFolderManager = get(),
relativeDateTimeFormatter = get(),
featureFlagProvider = get(),
contactLetterBitmapCreator = get(),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import com.fsck.k9.ui.changelog.RecentChangesActivity
import com.fsck.k9.ui.changelog.RecentChangesViewModel
import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import com.fsck.k9.ui.messagelist.MessageListFragmentBridgeContract.MessageListFragmentListener
import com.fsck.k9.ui.messagelist.MessageListFragmentBridgeContract.MessageListFragmentListener.Companion.MAX_PROGRESS
import com.fsck.k9.ui.messagelist.debug.AuthDebugActions
Expand All @@ -80,10 +81,8 @@ import java.util.concurrent.Future
import kotlin.time.ExperimentalTime
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import net.jcip.annotations.GuardedBy
Expand Down Expand Up @@ -183,6 +182,7 @@ class LegacyMessageListFragment :

private val contactRepository: ContactRepository by inject()
private val avatarMonogramCreator: AvatarMonogramCreator by inject()
private val relativeDateTimeFormatter: RelativeDateTimeFormatter by inject()

private val chooseFolderForMoveLauncher: ActivityResultLauncher<ChooseFolderResultContract.Input> =
registerForActivityResult(ChooseFolderResultContract(ChooseFolderActivity.Action.MOVE)) { result ->
Expand Down Expand Up @@ -318,18 +318,25 @@ class LegacyMessageListFragment :

adapter = createMessageListAdapter()

var observedDateTimeFormat = messageListSettings.dateTimeFormat
generalSettingsManager.getSettingsFlow()
/**
* Skips the first emitted item from the settings flow,
* since the initial value of `showingThreadedList` is taken
* from the fragment's arguments rather than the flow.
* since initial display settings are taken from fragment state and current preferences.
*/
.drop(1)
.map { it.display.inboxSettings.isThreadedViewEnabled }
.distinctUntilChanged()
.onEach {
showingThreadedList = it
loadMessageList(forceUpdate = true)
.onEach { generalSettings ->
val isThreadedViewEnabled = generalSettings.display.inboxSettings.isThreadedViewEnabled
if (showingThreadedList != isThreadedViewEnabled) {
showingThreadedList = isThreadedViewEnabled
loadMessageList(forceUpdate = true)
}

val dateTimeFormat = generalSettings.display.visualSettings.messageListSettings.dateTimeFormat
if (observedDateTimeFormat != dateTimeFormat) {
observedDateTimeFormat = dateTimeFormat
adapter.refreshFormattedDates()
}
}
.launchIn(lifecycleScope)

Expand Down Expand Up @@ -405,6 +412,9 @@ class LegacyMessageListFragment :
featureFlagProvider = featureFlagProvider,
contactRepository = contactRepository,
avatarMonogramCreator = avatarMonogramCreator,
formatDate = { timestamp ->
relativeDateTimeFormatter.formatDate(timestamp, messageListSettings.dateTimeFormat)
},
).apply {
activeMessage = this@LegacyMessageListFragment.activeMessage
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class MessageListAdapter internal constructor(
private val featureFlagProvider: FeatureFlagProvider,
private val contactRepository: ContactRepository,
private val avatarMonogramCreator: AvatarMonogramCreator,
private val formatDate: (Long) -> String,
) : RecyclerView.Adapter<MessageListViewHolder>() {

val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme)
Expand Down Expand Up @@ -170,6 +171,10 @@ class MessageListAdapter internal constructor(
return viewItems[position].viewType
}

fun refreshFormattedDates() {
notifyItemRangeChanged(0, itemCount)
}

private fun getItem(position: Int): MessageListItem = (viewItems[position] as MessageListViewItem.Message).item

fun getItemById(uniqueId: Long): MessageListItem? {
Expand Down Expand Up @@ -263,18 +268,19 @@ class MessageListAdapter internal constructor(

TYPE_MESSAGE -> {
val messageListItem = getItem(position)
val formattedMessageListItem = messageListItem.withFormattedDate()
val result = featureFlagProvider.provide(UseComposeForMessageListItems)
if (result.isEnabled()) {
val messageViewHolder = holder as ComposableMessageViewHolder
messageViewHolder.bind(
item = messageListItem,
item = formattedMessageListItem,
isActive = isActiveMessage(messageListItem),
isSelected = isSelected(messageListItem),
)
} else {
val messageViewHolder = holder as MessageViewHolder
messageViewHolder.bind(
messageListItem = messageListItem,
messageListItem = formattedMessageListItem,
isActive = isActiveMessage(messageListItem),
isSelected = isSelected(messageListItem),
)
Expand All @@ -301,6 +307,10 @@ class MessageListAdapter internal constructor(
item.messageUid == activeMessage.uid
}

private fun MessageListItem.withFormattedDate(): MessageListItem {
return copy(displayMessageDateTime = formatDate(messageDate))
}

fun isSelected(item: MessageListItem): Boolean {
return item.uniqueId in selected
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ class MessageListItemMapper(
private val account: LegacyAccount,
private val messageListPreferencesManager: MessageListPreferencesManager,
private val outboxFolderManager: OutboxFolderManager,
private val formatDate: (Long) -> String,
private val contactLetterBitmapCreator: ContactLetterBitmapCreator?,
) : MessageMapper<MessageListItem> {

Expand All @@ -39,7 +38,6 @@ class MessageListItemMapper(
} else {
messageHelper.getSenderDisplayName(displayAddress)
}
val displayMessageDateTime = formatDate(message.messageDate)

return MessageListItem(
account,
Expand All @@ -49,7 +47,7 @@ class MessageListItemMapper(
message.internalDate,
displayName,
displayAddress,
displayMessageDateTime,
displayMessageDateTime = "undefined",
previewText,
isMessageEncrypted,
message.isRead,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import com.fsck.k9.helper.MessageHelper
import com.fsck.k9.mailstore.LocalStoreProvider
import com.fsck.k9.mailstore.MessageColumns
import com.fsck.k9.search.getLegacyAccounts
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
import net.thunderbird.core.android.account.LegacyAccount
import net.thunderbird.core.android.account.LegacyAccountManager
import net.thunderbird.core.android.account.SortType
Expand All @@ -27,7 +26,6 @@ class MessageListLoader(
private val messageHelper: MessageHelper,
private val messageListPreferencesManager: MessageListPreferencesManager,
private val outboxFolderManager: OutboxFolderManager,
private val relativeDateTimeFormatter: RelativeDateTimeFormatter,
private val featureFlagProvider: FeatureFlagProvider,
private val contactLetterBitmapCreator: ContactLetterBitmapCreator,
) {
Expand Down Expand Up @@ -65,12 +63,6 @@ class MessageListLoader(
account,
messageListPreferencesManager,
outboxFolderManager,
formatDate = { formatTime ->
relativeDateTimeFormatter.formatDate(
formatTime,
messageListPreferencesManager.getConfig().dateTimeFormat,
)
},
contactLetterBitmapCreator = contactLetterBitmapCreator.takeIf {
featureFlagProvider.provide(MessageListFeatureFlags.UseComposeForMessageListItems).isEnabled()
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ class RelativeDateTimeFormatterTest : RobolectricTest() {

assertThat(displayDate).isEqualTo("2019-12-31 23:59")
}

private fun setClockTo(time: String) {
val dateTime = LocalDateTime.parse(time)
val timeInMillis = dateTime.toEpochMillis()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,7 @@ class MessageListAdapterTest : RobolectricTest() {
featureFlagProvider = FakeFeatureFlagProvider(),
avatarMonogramCreator = mock(),
contactRepository = mock(),
formatDate = { "12:34" },
)
}

Expand Down
Loading