diff --git a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java index 51f66ec3237..40263f85333 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BaseActionBarActivity.java @@ -21,13 +21,21 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper; import org.thoughtcrime.securesms.conversation.v2.WindowUtil; import org.thoughtcrime.securesms.util.ActivityUtilitiesKt; +import org.thoughtcrime.securesms.util.DateUtil; import org.thoughtcrime.securesms.util.ThemeState; import org.thoughtcrime.securesms.util.UiModeUtilities; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import network.loki.messenger.R; +@AndroidEntryPoint public abstract class BaseActionBarActivity extends AppCompatActivity { private static final String TAG = BaseActionBarActivity.class.getSimpleName(); + + @Inject DateUtil dateUtil; + public ThemeState currentThemeState; private TextSecurePreferences getPreferences() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java index 6544c2ab89e..f52f1ffecc5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/MediaPreviewActivity.java @@ -77,12 +77,10 @@ import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.util.AttachmentUtil; -import org.thoughtcrime.securesms.util.DateUtils; import org.thoughtcrime.securesms.util.SaveAttachmentTask; import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment; import java.io.IOException; -import java.util.Locale; import java.util.WeakHashMap; import network.loki.messenger.R; @@ -243,7 +241,7 @@ private void updateActionBar() { CharSequence relativeTimeSpan; if (mediaItem.date > 0) { - relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date); + relativeTimeSpan = dateUtil.format(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 cabb583085d..7573d9ffd4e 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 @@ -153,6 +153,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe @Inject lateinit var storage: Storage @Inject lateinit var reactionDb: ReactionDatabase @Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory + @Inject lateinit var dateUtil: DateUtil private val screenshotObserver by lazy { ScreenshotObserver(this, Handler(Looper.getMainLooper())) { @@ -1727,7 +1728,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val body = MentionUtilities.highlightMentions(message.body, viewModel.threadId, this) if (TextUtils.isEmpty(body)) { continue } if (messageSize > 1) { - val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp) + val formattedTimestamp = dateUtil.format(message.timestamp) builder.append("$formattedTimestamp: ") } builder.append(body) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java index 1d81325e034..1f5276a9d1b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java @@ -43,18 +43,21 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.util.AnimationCompleteListener; -import org.thoughtcrime.securesms.util.DateUtils; +import org.thoughtcrime.securesms.util.DateUtil; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.Locale; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import kotlin.Unit; import network.loki.messenger.R; +@AndroidEntryPoint public final class ConversationReactionOverlay extends FrameLayout { - + @Inject DateUtil dateUtil; public static final float LONG_PRESS_SCALE_FACTOR = 0.95f; private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); @@ -169,7 +172,7 @@ public void show(@NonNull Activity activity, conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight())); conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot)); TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp); - conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp())); + conversationTimestamp.setText(dateUtil.format(messageRecord.getTimestamp())); updateConversationTimestamp(messageRecord); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 3e370104ead..c8e1a910b86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -7,12 +7,18 @@ import android.view.View import android.widget.LinearLayout import androidx.core.content.res.ResourcesCompat import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewControlMessageBinding import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.util.DateUtil +import javax.inject.Inject +@AndroidEntryPoint class ControlMessageView : LinearLayout { + @Inject lateinit var dateUtil: DateUtil + private lateinit var binding: ViewControlMessageBinding // region Lifecycle @@ -28,7 +34,7 @@ class ControlMessageView : LinearLayout { // region Updating fun bind(message: MessageRecord, previous: MessageRecord?) { - binding.dateBreakTextView.showDateBreak(message, previous) + binding.dateBreakTextView.showDateBreak(dateUtil, message, previous) binding.iconImageView.visibility = View.GONE var messageBody: CharSequence = message.getDisplayBody(context) binding.root.contentDescription= null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/MessageUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/MessageUtilities.kt index 2eed1ee614f..afa567bfe29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/MessageUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/MessageUtilities.kt @@ -3,13 +3,12 @@ package org.thoughtcrime.securesms.conversation.v2.messages import android.widget.TextView import androidx.core.view.isVisible import org.thoughtcrime.securesms.database.model.MessageRecord -import org.thoughtcrime.securesms.util.DateUtils -import java.util.Locale +import org.thoughtcrime.securesms.util.DateUtil private const val maxTimeBetweenBreaks = 5 * 60 * 1000L // 5 minutes -fun TextView.showDateBreak(message: MessageRecord, previous: MessageRecord?) { +fun TextView.showDateBreak(dateUtil: DateUtil, message: MessageRecord, previous: MessageRecord?) { val showDateBreak = (previous == null || message.timestamp - previous.timestamp > maxTimeBetweenBreaks) isVisible = showDateBreak - text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else "" + text = if (showDateBreak) dateUtil.format(message.timestamp) else "" } 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 2c04bcfce3a..00b2428d883 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 @@ -47,12 +47,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.home.UserDetailsBottomSheet import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.disableClipping -import org.thoughtcrime.securesms.util.toDp -import org.thoughtcrime.securesms.util.toPx +import org.thoughtcrime.securesms.util.* import java.util.Date -import java.util.Locale import javax.inject.Inject import kotlin.math.abs import kotlin.math.min @@ -62,6 +58,7 @@ import kotlin.math.sqrt @AndroidEntryPoint class VisibleMessageView : LinearLayout { + @Inject lateinit var dateUtil: DateUtil @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase @Inject lateinit var lokiApiDb: LokiAPIDatabase @@ -193,7 +190,7 @@ class VisibleMessageView : LinearLayout { binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID // Date break val showDateBreak = isStartOfMessageCluster || snIsSelected - binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null + binding.dateBreakTextView.text = if (showDateBreak) dateUtil.format(message.timestamp) else null binding.dateBreakTextView.isVisible = showDateBreak // Message status indicator if (message.isOutgoing) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt index 6f26c6ae3ae..5befa3f58df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/AppModule.kt @@ -8,6 +8,8 @@ import org.session.libsession.utilities.AppTextSecurePreferences import org.session.libsession.utilities.TextSecurePreferences import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.DefaultConversationRepository +import org.thoughtcrime.securesms.util.AndroidClock +import org.thoughtcrime.securesms.util.Clock @Module @InstallIn(SingletonComponent::class) @@ -19,4 +21,7 @@ abstract class AppModule { @Binds abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository + @Binds + abstract fun bindAndroidClock(androidClock: AndroidClock): Clock + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt index c6a6e1f7f5d..c85e99342a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt @@ -6,7 +6,6 @@ import android.graphics.Typeface import android.graphics.drawable.ColorDrawable import android.util.AttributeSet import android.util.TypedValue -import android.view.LayoutInflater import android.view.View import android.widget.LinearLayout import androidx.core.content.ContextCompat @@ -20,11 +19,13 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.DateUtil import org.thoughtcrime.securesms.util.getAccentColor -import java.util.Locale +import javax.inject.Inject class ConversationView : LinearLayout { + @Inject lateinit var dateUtil: DateUtil + private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) } private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null @@ -85,7 +86,7 @@ class ConversationView : LinearLayout { val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.conversationViewDisplayNameTextView.text = senderDisplayName - binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) + binding.timestampTextView.text = dateUtil.format(thread.date) val recipient = thread.recipient binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt index 9b1e01a54a8..7ebf2137932 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt @@ -63,15 +63,8 @@ import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.onboarding.SeedActivity import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate import org.thoughtcrime.securesms.preferences.SettingsActivity -import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities -import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.IP2Country -import org.thoughtcrime.securesms.util.disableClipping -import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.show -import org.thoughtcrime.securesms.util.themeState +import org.thoughtcrime.securesms.util.* import java.io.IOException -import java.util.Locale import javax.inject.Inject @AndroidEntryPoint @@ -84,6 +77,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), private lateinit var glide: GlideRequests private var broadcastReceiver: BroadcastReceiver? = null + @Inject lateinit var dateUtil: DateUtil @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var mmsSmsDatabase: MmsSmsDatabase @Inject lateinit var recipientDatabase: RecipientDatabase @@ -100,7 +94,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), HomeAdapter(context = this, listener = this) } - private val globalSearchAdapter = GlobalSearchAdapter { model -> + private val globalSearchAdapter = GlobalSearchAdapter(dateUtil) { model -> when (model) { is GlobalSearchAdapter.Model.Message -> { val threadId = model.messageResult.threadId @@ -290,11 +284,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) { with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) { unreadCountTextView.text = messageRequestCount.toString() - timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString( - this@HomeActivity, - Locale.getDefault(), - threadDb.latestUnapprovedConversationTimestamp - ) + timestampTextView.text = dateUtil.format(threadDb.latestUnapprovedConversationTimestamp) root.setOnClickListener { showMessageRequests() } root.setOnLongClickListener { hideMessageRequests(); true } root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt index fab8bca9989..ca4d826a8c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapter.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import androidx.annotation.StringRes import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding import network.loki.messenger.databinding.ViewGlobalSearchResultBinding @@ -13,10 +14,14 @@ import org.session.libsession.utilities.GroupRecord import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.search.model.MessageResult +import org.thoughtcrime.securesms.util.DateUtil import java.security.InvalidParameterException import org.session.libsession.messaging.contacts.Contact as ContactModel -class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter() { +class GlobalSearchAdapter( + private val dateUtil: DateUtil, + private val modelCallback: (Model) -> Unit +) : RecyclerView.Adapter() { companion object { const val HEADER_VIEW_TYPE = 0 @@ -47,8 +52,10 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi } else { ContentView( LayoutInflater.from(parent.context) - .inflate(R.layout.view_global_search_result, parent, false) - , modelCallback) + .inflate(R.layout.view_global_search_result, parent, false), + dateUtil, + modelCallback + ) } override fun onBindViewHolder( @@ -69,7 +76,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - onBindViewHolder(holder,position, mutableListOf()) + onBindViewHolder(holder, position, mutableListOf()) } class HeaderView(view: View) : RecyclerView.ViewHolder(view) { @@ -87,8 +94,11 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi } } - class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) { - + class ContentView( + view: View, + private val dateUtil: DateUtil, + private val modelCallback: (Model) -> Unit + ) : RecyclerView.ViewHolder(view) { val binding = ViewGlobalSearchResultBinding.bind(view).apply { searchResultProfilePicture.root.glide = GlideApp.with(root) } @@ -102,7 +112,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi when (model) { is Model.GroupConversation -> bindModel(query, model) is Model.Contact -> bindModel(query, model) - is Model.Message -> bindModel(query, model) + is Model.Message -> bindModel(dateUtil, query, model) is Model.SavedMessages -> bindModel(model) is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView") } @@ -112,14 +122,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi } data class MessageModel( - val threadRecipient: Recipient, - val messageRecipient: Recipient, - val messageSnippet: String + val threadRecipient: Recipient, + val messageRecipient: Recipient, + val messageSnippet: String ) sealed class Model { data class Header(@StringRes val title: Int) : Model() - data class SavedMessages(val currentUserPublicKey: String): Model() + data class SavedMessages(val currentUserPublicKey: String) : Model() data class Contact(val contact: ContactModel) : Model() data class GroupConversation(val groupRecord: GroupRecord) : Model() data class Message(val messageResult: MessageResult, val unread: Int) : Model() diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt index 2c64ded866c..5367f2079d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/home/search/GlobalSearchAdapterUtils.kt @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages -import org.thoughtcrime.securesms.util.DateUtils +import org.thoughtcrime.securesms.util.DateUtil import org.thoughtcrime.securesms.util.SearchUtil import java.util.Locale import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel @@ -125,7 +125,7 @@ fun ContentView.bindModel(model: SavedMessages) { binding.searchResultSavedMessages.isVisible = true } -fun ContentView.bindModel(query: String?, model: Message) { +fun ContentView.bindModel(dateUtil: DateUtil, query: String?, model: Message) { binding.searchResultProfilePicture.root.isVisible = true binding.searchResultSavedMessages.isVisible = false binding.searchResultTimestamp.isVisible = true @@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) { // if (hasUnreads) { // binding.unreadCountTextView.text = model.unread.toString() // } - binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs) + binding.searchResultTimestamp.text = dateUtil.format(model.messageResult.sentTimestampMs) binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient) val textSpannable = SpannableStringBuilder() if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt index 9a8d0612975..bea443dd1fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestView.kt @@ -6,16 +6,19 @@ import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout import androidx.recyclerview.widget.RecyclerView +import dagger.hilt.android.AndroidEntryPoint import network.loki.messenger.R import network.loki.messenger.databinding.ViewMessageRequestBinding import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.mms.GlideRequests -import org.thoughtcrime.securesms.util.DateUtils -import java.util.Locale +import org.thoughtcrime.securesms.util.DateUtil +@AndroidEntryPoint class MessageRequestView : LinearLayout { + lateinit var dateUtil: DateUtil + private lateinit var binding: ViewMessageRequestBinding private val screenWidth = Resources.getSystem().displayMetrics.widthPixels var thread: ThreadRecord? = null @@ -38,7 +41,7 @@ class MessageRequestView : LinearLayout { val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString() binding.displayNameTextView.text = senderDisplayName - binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date) + binding.timestampTextView.text = dateUtil.format(thread.date) val rawSnippet = thread.getDisplayBody(context) val snippet = highlightMentions(rawSnippet, thread.threadId, context) binding.snippetTextView.text = snippet 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 074278cb92a..63448ccecc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/BackupUtil.kt @@ -73,15 +73,6 @@ object BackupUtil { return prefList } - @JvmStatic - fun getLastBackupTimeString(context: Context, locale: Locale): String { - val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime() - if (timestamp == null) { - return context.getString(R.string.BackupUtil_never) - } - return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time) - } - @JvmStatic fun getLastBackup(context: Context): BackupFileRecord? { return DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFile() diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Clock.kt b/app/src/main/java/org/thoughtcrime/securesms/util/Clock.kt new file mode 100644 index 00000000000..ee9ae8ed134 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Clock.kt @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.util + +import javax.inject.Inject +import javax.inject.Singleton + +interface Clock { + val currentTimeMillis: Long +} + +@Singleton +class AndroidClock @Inject constructor(): Clock { + override val currentTimeMillis: Long + get() = System.currentTimeMillis() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtil.kt new file mode 100644 index 00000000000..7943974ad9e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtil.kt @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.util + +import android.content.Context +import dagger.hilt.android.qualifiers.ApplicationContext +import network.loki.messenger.R +import org.thoughtcrime.securesms.util.DateUtils.isSameDay +import java.util.* +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class DateUtil @Inject constructor ( + @ApplicationContext private val context: Context, + private val clock: Clock +) { + /** + * If the message was sent/received on the same day we show: 12 hour time + am/pm + * 12:43 pm + * If the message was sent/received on the same week we show: word abbreviation day of week + 12 hour time + am/pm + * Tue 12:34 pm + * If the message was sent/received in the current year we show, word abbreviation of month, date (day) and 12 hour time + am/pm + * Mar 9 12:34 pm + * If the message was sent or received outside of the current year, then we show dd/mm/yy + * 09/03/23 + */ + @JvmOverloads + fun format(timestamp: Long, locale: Locale = Locale.getDefault()): String { + if (isWithin(timestamp, 1, TimeUnit.MINUTES)) { + return context.getString(R.string.DateUtils_just_now) + } + return when { + isToday(timestamp) -> context.hourFormat + isWithin(timestamp, 6, TimeUnit.DAYS) -> "EEE " + context.hourFormat + isThisYear(timestamp) -> "MMM d " + context.hourFormat + else -> "MMM d " + context.hourFormat + ", yyyy" + }.let { DateUtils.getFormattedDateTime(timestamp, it, locale) } + } + + private fun isThisYear(millis: Long): Boolean = isSameYear(millis, clock.currentTimeMillis) + + fun isToday(time: Long): Boolean = isSameDay(time, clock.currentTimeMillis) + + fun isWithin(millis: Long, span: Long, unit: TimeUnit): Boolean = + clock.currentTimeMillis - millis <= unit.toMillis(span) +} + +fun isSameYear(millis: Long, millisOther: Long): Boolean = Calendar.getInstance().run { + apply { timeInMillis = millis }.get(Calendar.YEAR) == apply { timeInMillis = millisOther }.get(Calendar.YEAR) + } + +private val Context.hourFormat: String get() = DateUtils.getHourFormat(this) 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 874440f5de5..6bf695a4751 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java @@ -45,7 +45,7 @@ public class DateUtils extends android.text.format.DateUtils { 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) { + static boolean isWithin(final long millis, final long span, final TimeUnit unit) { return System.currentTimeMillis() - millis <= unit.toMillis(span); } @@ -66,20 +66,6 @@ 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; diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilTest.kt b/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilTest.kt new file mode 100644 index 00000000000..a4e48d4ea07 --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/DateUtilTest.kt @@ -0,0 +1,147 @@ +package org.thoughtcrime.securesms.util + +import android.app.Application +import androidx.test.core.app.ApplicationProvider +import org.junit.Assert.* +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.kotlin.whenever +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowDateFormat +import org.robolectric.shadows.ShadowSettings +import java.util.Locale +import java.util.TimeZone +import java.util.Calendar +import java.util.Calendar.JANUARY +import java.util.Calendar.NOVEMBER +import java.util.Calendar.DECEMBER + +@RunWith(ParameterizedRobolectricTestRunner::class) +@Config(application = TestApplication::class, shadows = [ShadowDateFormat::class]) +class DateUtilTest( + private val expected: String, + private val locale: Locale, + private val then: Long, + private val now: Long, + private val is24HourTime: Boolean +) { + private val clock = Mockito.mock(Clock::class.java) + private val context = ApplicationProvider.getApplicationContext() + + private val dateUtil = DateUtil(context, clock) + + @Test + fun test() { + whenever(clock.currentTimeMillis).thenReturn(now) + ShadowSettings.set24HourTimeFormat(is24HourTime) + val timestamp = dateUtil.format(then, locale) + assertEquals(expected, timestamp) + } + + companion object { + private val zone = TimeZone.getTimeZone("GMT") + + @JvmStatic + @ParameterizedRobolectricTestRunner.Parameters(name = "{index}: myMethod({0}, {1})") + fun parameters(): List> = listOf( + timespan( + millis(2001, JANUARY), + millis(2001, JANUARY), + assertion(Locale.US, "Now", "Now"), + assertion(Locale.CHINA, "Now", "Now"), + assertion(Locale.ENGLISH, "Now", "Now") + ), + timespan( + millis(2001, JANUARY), + millis(2001, JANUARY), + assertion(Locale.US, "Now", "Now"), + ), + timespan( + millis(2001, DECEMBER, 30), + millis(2001, DECEMBER, 30, second = 59), + assertion(Locale.US, "Now", "Now"), + ), + timespan( + millis(2001, DECEMBER, 30), + millis(2001, DECEMBER, 30, minute = 1), + assertion(Locale.US, "Now", "Now") + ), + timespan( + millis(2001, DECEMBER, 30), + millis(2001, DECEMBER, 30, minute = 1, second = 1), + assertion(Locale.US, "10:30 AM", "10:30"), + assertion(Locale.JAPAN, "10:30 午前", "10:30") + ), + timespan( + millis(2001, DECEMBER, 30), + millis(2001, DECEMBER, 30, minute = 1, second = 1), + assertion(Locale.US, "10:30 AM", "10:30") + ), + timespan( + millis(2001, DECEMBER, 30, hour = 12, minute = 15), + millis(2001, DECEMBER, 30, hour = 12, minute = 16, second = 1), + assertion(Locale.US, "10:45 PM", "22:45") + ), + timespan( + millis(2001, DECEMBER, 25), + millis(2001, DECEMBER, 30), + assertion(Locale.US, "Tue 10:30 AM", "Tue 10:30") + ), + timespan( + millis(2001, NOVEMBER, 25), + millis(2001, DECEMBER, 10), + assertion(Locale.US, "Nov 25 10:30 AM", "Nov 25 10:30") + ), + timespan( + millis(2001, JANUARY), + millis(2003, JANUARY), + assertion(Locale.US, "Jan 1 10:30 AM, 2001", "Jan 1 10:30, 2001") + ), + ).flatten() + + private fun assertion( + locale: Locale, + expected: String, + expected24HourTime: String + ): Timespan.() -> List> = { + listOf( + assertion(locale, expected, false), + assertion(locale, expected24HourTime, true) + ) + } + + private fun timespan( + start: Locale.() -> Long, + end: Locale.() -> Long, + vararg assertions: Timespan.() -> List> + ): List> = Timespan(start, end).run { assertions.map { it() } }.flatten() + + private fun millis( + year: Int, + month: Int = 0, + date: Int = 1, + hour: Int = 0, + minute: Int = 0, + second: Int = 0 + ): (Locale) -> Long = { + Calendar.getInstance(zone, it) + .apply { set(year, month, date, hour, minute, second) } + .timeInMillis + } + + class Timespan( + val start: (Locale) -> Long, + val end: (Locale) -> Long + ) { + fun assertion( + locale: Locale, + expected: String, + is24HourTime: Boolean + ): Array = arrayOf(expected, locale, start(locale), end(locale), is24HourTime) + } + } +} + +class TestApplication : Application() diff --git a/app/src/test/resources/robolectric.properties b/app/src/test/resources/robolectric.properties new file mode 100644 index 00000000000..d27d2bdc6e7 --- /dev/null +++ b/app/src/test/resources/robolectric.properties @@ -0,0 +1 @@ +sdk=29 \ No newline at end of file