From 183f013c31beabed9b64bd8cb0fd982c7aa84e64 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 8 Jul 2021 13:37:08 +1000 Subject: [PATCH] Show date break header by hour instead of by day Also ditch relative timestamps in favor of absolute ones --- .../securesms/MediaPreviewActivity.java | 2 +- .../conversation/v2/ConversationActivityV2.kt | 2 +- .../v2/messages/VisibleMessageView.kt | 10 +- .../securesms/loki/views/ConversationView.kt | 2 +- .../thoughtcrime/securesms/util/BackupUtil.kt | 2 +- .../securesms/util/DateUtils.java | 106 +++++++++--------- 6 files changed, 65 insertions(+), 59 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 0b36a73eed..e28b419880 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -189,7 +189,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im CharSequence relativeTimeSpan; if (mediaItem.date > 0) { - relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date); + relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date); } else { relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 7ca866cd76..d5ceda8d72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -1141,7 +1141,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe for (message in sortedMessages) { val body = MentionUtilities.highlightMentions(message.body, message.threadId, this) if (TextUtils.isEmpty(body)) { continue } - val formattedTimestamp = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), message.timestamp) + val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) builder.append("$formattedTimestamp: $body").append('\n') } if (builder.isNotEmpty() && builder[builder.length - 1] == '\n') { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index f938007ada..147e18906f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -110,11 +110,11 @@ class VisibleMessageView : LinearLayout { senderNameTextView.visibility = View.GONE } // Date break - val showDateBreak = (previous == null || !DateUtils.isSameDay(message.timestamp, previous.timestamp)) + val showDateBreak = (previous == null || !DateUtils.isSameHour(message.timestamp, previous.timestamp)) dateBreakTextView.isVisible = showDateBreak - dateBreakTextView.text = if (showDateBreak) DateUtils.getRelativeDate(context, Locale.getDefault(), message.timestamp) else "" + dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else "" // Timestamp - messageTimestampTextView.text = DateUtils.getExtendedRelativeTimeSpanString(context, Locale.getDefault(), message.timestamp) + messageTimestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) // Margins val startPadding: Int if (isGroupThread) { @@ -177,10 +177,10 @@ class VisibleMessageView : LinearLayout { private fun isEndOfMessageCluster(current: MessageRecord, next: MessageRecord?, isGroupThread: Boolean): Boolean { return if (isGroupThread) { - next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp) + next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp) || current.recipient.address != next.recipient.address } else { - next == null || next.isUpdate || !DateUtils.isSameDay(current.timestamp, next.timestamp) + next == null || next.isUpdate || !DateUtils.isSameHour(current.timestamp, next.timestamp) || current.isOutgoing != next.isOutgoing } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt index 893c73019e..f4341b5ed1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/views/ConversationView.kt @@ -58,7 +58,7 @@ class ConversationView : LinearLayout { profilePictureView.update(thread.recipient, thread.threadId) val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() conversationViewDisplayNameTextView.text = senderDisplayName - timestampTextView.text = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), thread.date) + timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE val rawSnippet = thread.getDisplayBody(context) val snippet = highlightMentions(rawSnippet, thread.threadId, context) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt index e38df7391b..5d2e84ed7a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -118,7 +118,7 @@ object BackupUtil { if (timestamp == null) { return context.getString(R.string.BackupUtil_never) } - return DateUtils.getExtendedRelativeTimeSpanString(context, locale, timestamp.time) + return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time) } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java index cb822f1eeb..7860e46242 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -28,6 +28,7 @@ import org.session.libsignal.utilities.Log; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Calendar; import java.util.Date; import java.util.Locale; import java.util.concurrent.TimeUnit; @@ -40,8 +41,9 @@ import network.loki.messenger.R; public class DateUtils extends android.text.format.DateUtils { @SuppressWarnings("unused") - private static final String TAG = DateUtils.class.getSimpleName(); - private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + private static final String TAG = DateUtils.class.getSimpleName(); + private static final SimpleDateFormat DAY_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd"); + private static final SimpleDateFormat HOUR_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHH"); private static boolean isWithin(final long millis, final long span, final TimeUnit unit) { return System.currentTimeMillis() - millis <= unit.toMillis(span); @@ -60,56 +62,21 @@ public class DateUtils extends android.text.format.DateUtils { return new SimpleDateFormat(localizedPattern, locale).format(new Date(time)); } - public static String getBriefRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) { - if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { - return c.getString(R.string.DateUtils_just_now); - } else if (isWithin(timestamp, 1, TimeUnit.HOURS)) { - int mins = convertDelta(timestamp, TimeUnit.MINUTES); - return c.getResources().getString(R.string.DateUtils_minutes_ago, mins); - } else if (isWithin(timestamp, 1, TimeUnit.DAYS)) { - int hours = convertDelta(timestamp, TimeUnit.HOURS); - return c.getResources().getQuantityString(R.plurals.hours_ago, hours, hours); - } else if (isWithin(timestamp, 6, TimeUnit.DAYS)) { - return getFormattedDateTime(timestamp, "EEE", locale); - } else if (isWithin(timestamp, 365, TimeUnit.DAYS)) { - return getFormattedDateTime(timestamp, "MMM d", locale); - } else { - return getFormattedDateTime(timestamp, "MMM d, yyyy", locale); - } + public static String getHourFormat(Context c) { + return (DateFormat.is24HourFormat(c)) ? "HH:mm" : "hh:mm a"; } - public static String getExtendedRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) { + public static String getDisplayFormattedTimeSpanString(final Context c, final Locale locale, final long timestamp) { if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { return c.getString(R.string.DateUtils_just_now); - } else if (isWithin(timestamp, 1, TimeUnit.HOURS)) { - int mins = (int)TimeUnit.MINUTES.convert(System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS); - return c.getResources().getString(R.string.DateUtils_minutes_ago, mins); - } else { - StringBuilder format = new StringBuilder(); - if (isWithin(timestamp, 6, TimeUnit.DAYS)) format.append("EEE "); - else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format.append("MMM d, "); - else format.append("MMM d, yyyy, "); - - if (DateFormat.is24HourFormat(c)) format.append("HH:mm"); - else format.append("hh:mm a"); - - return getFormattedDateTime(timestamp, format.toString(), locale); - } - } - - public static String getDayPrecisionTimeSpanString(Context context, Locale locale, long timestamp) { - SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd"); - - if (simpleDateFormat.format(System.currentTimeMillis()).equals(simpleDateFormat.format(timestamp))) { - return context.getString(R.string.DeviceListItem_today); + } else if (isToday(timestamp)) { + return getFormattedDateTime(timestamp, getHourFormat(c), locale); + } else if (isWithin(timestamp, 6, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "EEE " + getHourFormat(c), locale); + } else if (isWithin(timestamp, 365, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c), locale); } else { - String format; - - if (isWithin(timestamp, 6, TimeUnit.DAYS)) format = "EEE "; - else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format = "MMM d"; - else format = "MMM d, yyy"; - - return getFormattedDateTime(timestamp, format, locale); + return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale); } } @@ -139,11 +106,11 @@ public class DateUtils extends android.text.format.DateUtils { } public static boolean isSameDay(long t1, long t2) { - return DATE_FORMAT.format(new Date(t1)).equals(DATE_FORMAT.format(new Date(t2))); + return DAY_PRECISION_DATE_FORMAT.format(new Date(t1)).equals(DAY_PRECISION_DATE_FORMAT.format(new Date(t2))); } - public static boolean isSameExtendedRelativeTimestamp(@NonNull Context context, @NonNull Locale locale, long t1, long t2) { - return getExtendedRelativeTimeSpanString(context, locale, t1).equals(getExtendedRelativeTimeSpanString(context, locale, t2)); + public static boolean isSameHour(long t1, long t2) { + return HOUR_PRECISION_DATE_FORMAT.format(new Date(t1)).equals(HOUR_PRECISION_DATE_FORMAT.format(new Date(t2))); } private static String getLocalizedPattern(String template, Locale locale) { @@ -178,4 +145,43 @@ public class DateUtils extends android.text.format.DateUtils { return -1; } } + + // region Deprecated + public static String getBriefRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) { + if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { + return c.getString(R.string.DateUtils_just_now); + } else if (isWithin(timestamp, 1, TimeUnit.HOURS)) { + int mins = convertDelta(timestamp, TimeUnit.MINUTES); + return c.getResources().getString(R.string.DateUtils_minutes_ago, mins); + } else if (isWithin(timestamp, 1, TimeUnit.DAYS)) { + int hours = convertDelta(timestamp, TimeUnit.HOURS); + return c.getResources().getQuantityString(R.plurals.hours_ago, hours, hours); + } else if (isWithin(timestamp, 6, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "EEE", locale); + } else if (isWithin(timestamp, 365, TimeUnit.DAYS)) { + return getFormattedDateTime(timestamp, "MMM d", locale); + } else { + return getFormattedDateTime(timestamp, "MMM d, yyyy", locale); + } + } + + public static String getExtendedRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) { + if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { + return c.getString(R.string.DateUtils_just_now); + } else if (isWithin(timestamp, 1, TimeUnit.HOURS)) { + int mins = (int)TimeUnit.MINUTES.convert(System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS); + return c.getResources().getString(R.string.DateUtils_minutes_ago, mins); + } else { + StringBuilder format = new StringBuilder(); + if (isWithin(timestamp, 6, TimeUnit.DAYS)) format.append("EEE "); + else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format.append("MMM d, "); + else format.append("MMM d, yyyy, "); + + if (DateFormat.is24HourFormat(c)) format.append("HH:mm"); + else format.append("hh:mm a"); + + return getFormattedDateTime(timestamp, format.toString(), locale); + } + } + // endregion }