From 183f013c31beabed9b64bd8cb0fd982c7aa84e64 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 8 Jul 2021 13:37:08 +1000 Subject: [PATCH 1/2] 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 | 174 +++++++++--------- 6 files changed, 99 insertions(+), 93 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,6 +62,91 @@ public class DateUtils extends android.text.format.DateUtils { return new SimpleDateFormat(localizedPattern, locale).format(new Date(time)); } + public static String getHourFormat(Context c) { + return (DateFormat.is24HourFormat(c)) ? "HH:mm" : "hh:mm a"; + } + + 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 (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 { + return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale); + } + } + + public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) { + String dateFormatPattern; + + if (DateFormat.is24HourFormat(context)) { + dateFormatPattern = getLocalizedPattern("MMM d, yyyy HH:mm:ss zzz", locale); + } else { + dateFormatPattern = getLocalizedPattern("MMM d, yyyy hh:mm:ss a zzz", locale); + } + + return new SimpleDateFormat(dateFormatPattern, locale); + } + + public static String getRelativeDate(@NonNull Context context, + @NonNull Locale locale, + long timestamp) + { + if (isToday(timestamp)) { + return context.getString(R.string.DateUtils_today); + } else if (isYesterday(timestamp)) { + return context.getString(R.string.DateUtils_yesterday); + } else { + return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale); + } + } + + public static boolean isSameDay(long t1, long t2) { + return DAY_PRECISION_DATE_FORMAT.format(new Date(t1)).equals(DAY_PRECISION_DATE_FORMAT.format(new Date(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) { + return DateFormat.getBestDateTimePattern(locale, template); + } + + /** + * e.g. 2020-09-04T19:17:51Z + * https://www.iso.org/iso-8601-date-and-time-format.html + * + * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. + * + * @return The timestamp if able to be parsed, otherwise -1. + */ + @SuppressLint("ObsoleteSdkInt") + public static long parseIso8601(@Nullable String date) { + SimpleDateFormat format; + if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); + } else { + format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); + } + + if (date.isEmpty()) { + return -1; + } + + try { + return format.parse(date).getTime(); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse date.", e); + 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); @@ -96,86 +183,5 @@ public class DateUtils extends android.text.format.DateUtils { 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 { - 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); - } - } - - public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) { - String dateFormatPattern; - - if (DateFormat.is24HourFormat(context)) { - dateFormatPattern = getLocalizedPattern("MMM d, yyyy HH:mm:ss zzz", locale); - } else { - dateFormatPattern = getLocalizedPattern("MMM d, yyyy hh:mm:ss a zzz", locale); - } - - return new SimpleDateFormat(dateFormatPattern, locale); - } - - public static String getRelativeDate(@NonNull Context context, - @NonNull Locale locale, - long timestamp) - { - if (isToday(timestamp)) { - return context.getString(R.string.DateUtils_today); - } else if (isYesterday(timestamp)) { - return context.getString(R.string.DateUtils_yesterday); - } else { - return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale); - } - } - - public static boolean isSameDay(long t1, long t2) { - return DATE_FORMAT.format(new Date(t1)).equals(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)); - } - - private static String getLocalizedPattern(String template, Locale locale) { - return DateFormat.getBestDateTimePattern(locale, template); - } - - /** - * e.g. 2020-09-04T19:17:51Z - * https://www.iso.org/iso-8601-date-and-time-format.html - * - * Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences. - * - * @return The timestamp if able to be parsed, otherwise -1. - */ - @SuppressLint("ObsoleteSdkInt") - public static long parseIso8601(@Nullable String date) { - SimpleDateFormat format; - if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault()); - } else { - format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault()); - } - - if (date.isEmpty()) { - return -1; - } - - try { - return format.parse(date).getTime(); - } catch (ParseException e) { - Log.w(TAG, "Failed to parse date.", e); - return -1; - } - } + // endregion } From 5be63cd2979aba08ed6252bc04f4573ab28b2063 Mon Sep 17 00:00:00 2001 From: Niels Andriesse Date: Thu, 8 Jul 2021 13:38:14 +1000 Subject: [PATCH 2/2] Update build number --- app/build.gradle | 2 +- .../securesms/loki/activities/HomeActivity.kt | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2d80c653b9..833622e129 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -143,7 +143,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.2' } -def canonicalVersionCode = 194 +def canonicalVersionCode = 195 def canonicalVersionName = "1.11.3" def postFixSize = 10 diff --git a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt index 2b5e2828db..a4b5e6437b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/loki/activities/HomeActivity.kt @@ -11,6 +11,7 @@ import android.text.Spannable import android.text.SpannableString import android.text.style.ForegroundColorSpan import android.util.DisplayMetrics +import android.util.Log import android.view.View import android.widget.RelativeLayout import android.widget.Toast @@ -35,8 +36,8 @@ import org.session.libsignal.utilities.ThreadUtils import org.session.libsignal.utilities.toHexString import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity -import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.loki.api.OpenGroupManager @@ -48,7 +49,10 @@ import org.thoughtcrime.securesms.loki.views.NewConversationButtonSetViewDelegat import org.thoughtcrime.securesms.loki.views.SeedReminderViewDelegate import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.mms.GlideRequests +import org.thoughtcrime.securesms.util.DateUtils import java.io.IOException +import java.util.* +import java.util.concurrent.TimeUnit class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickListener, SeedReminderViewDelegate, NewConversationButtonSetViewDelegate { private lateinit var glide: GlideRequests @@ -150,6 +154,16 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis } } EventBus.getDefault().register(this@HomeActivity) + testDateFormatting() + } + + private fun testDateFormatting() { + val timestamp = Date().time + Log.d("Test", getString(R.string.DateUtils_just_now)) + Log.d("Test", DateUtils.getFormattedDateTime(timestamp, DateUtils.getHourFormat(this), Locale.getDefault())) + Log.d("Test", DateUtils.getFormattedDateTime(timestamp, "EEE " + DateUtils.getHourFormat(this), Locale.getDefault())) + Log.d("Test", DateUtils.getFormattedDateTime(timestamp, "MMM d " + DateUtils.getHourFormat(this), Locale.getDefault())) + Log.d("Test", DateUtils.getFormattedDateTime(timestamp, "MMM d " + DateUtils.getHourFormat(this) + ", yyyy", Locale.getDefault())) } override fun onResume() {