Skip to content
Draft
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 @@ -3,11 +3,9 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.util.AttributeSet
import androidx.preference.CheckBoxPreference
import com.squareup.phrase.Phrase
import network.loki.messenger.R
import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
import org.thoughtcrime.securesms.ui.getSubbedString

class SwitchPreferenceCompat : CheckBoxPreference {
private var listener: OnPreferenceClickListener? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import org.session.libsession.utilities.getExpirationTypeDisplayValue
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.showSessionDialog
import org.thoughtcrime.securesms.ui.getSubbedCharSequence
import org.thoughtcrime.securesms.ui.getSubbedString
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.preferences;


import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Typeface;
Expand All @@ -9,20 +8,18 @@
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat;
import androidx.fragment.app.DialogFragment;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceGroupAdapter;
import androidx.preference.PreferenceScreen;
import androidx.preference.PreferenceViewHolder;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;

public abstract class CorrectedPreferenceFragment extends PreferenceFragmentCompat {

Expand Down
182 changes: 168 additions & 14 deletions app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,8 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.text.format.DateFormat
import androidx.compose.ui.text.capitalize
import org.session.libsession.utilities.TextSecurePreferences
import org.session.libsignal.utilities.Log
import java.text.DateFormat.SHORT
import java.text.DateFormat.getTimeInstance
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Calendar
Expand All @@ -44,6 +42,20 @@ object DateUtils : android.text.format.DateUtils() {
private val DAY_PRECISION_DATE_FORMAT = SimpleDateFormat("yyyyMMdd")
private val HOUR_PRECISION_DATE_FORMAT = SimpleDateFormat("yyyyMMddHH")

// Preferred date and time formats for the user - these are stored in the user's preferences.
// We set them as invalid on startup (range is 0..8 and 0..2 respectively) so we only have to
// retrieve the preference once rather than every time we wish to format a date or time.
// See: TextSecurePreferences.DATE_FORMAT_PREF and TextSecurePreferences for further details.
// Note: For testing you can just set these values to something which isn't -1 and it will
// bypass the retrieve-from-pref operation and use the associated format.
private var userPreferredTimeFormat: Int = -1
private var userPreferredDateFormat: Int = 7

// String for the actual date format pattern that `userPreferredDataFormat` equates to - we'll
// start it off as a sane default, and update it to either follow the system or a specific
// format as the user desires.
private var userPreferredDateFormatPattern = "dd/MM/YYYY"

private fun isWithin(millis: Long, span: Long, unit: TimeUnit): Boolean {
return System.currentTimeMillis() - millis <= unit.toMillis(span)
}
Expand Down Expand Up @@ -82,27 +94,169 @@ object DateUtils : android.text.format.DateUtils() {
FORMAT_SHOW_DATE).toString()
}

fun getFormattedDateTime(time: Long, template: String, locale: Locale): String {
// THIS IS THE METHOD THAT ACTUALLY GETS USED TO GET THE DATE TIME
fun getFormattedDateTime(timestamp: Long, template: String, locale: Locale): String {

Log.w("ACL", "Getting formatted datetime - template is: " + template)
val localizedPattern = getLocalizedPattern(template, locale)
return SimpleDateFormat(localizedPattern, locale).format(Date(time))
Log.w("ACL", "Which turns into localizedPattern: " + localizedPattern)
return SimpleDateFormat(template, locale).format(Date(timestamp))
}

// Method to get the user's preferred time format, whether that's 12-hour or 24-hour
fun getHourFormat(c: Context): String {
// If this is the first run..
if (userPreferredTimeFormat == -1) {
// ..update our preferred time format (will return -1 if no saved pref).
userPreferredTimeFormat = TextSecurePreferences.getTimeFormatPref(c)

// If no saved value was written we'll write 0 for "Follow system setting" - this will only run on first install
if (userPreferredTimeFormat == -1) {
userPreferredTimeFormat = 0
TextSecurePreferences.setTimeFormatPref(c, userPreferredTimeFormat)
}

// If the preferred time format is "Follow system setting" then we need to find out what the system setting is!
if (userPreferredTimeFormat == 0) {
val is24HourFormat = DateFormat.is24HourFormat(c)

// Set the time format we'll use to either 24 or 12 hours.
// IMPORTANT: We don't WRITE this to the pref - we just use it while the app is running!
// Note: See TextSecurePreferences.TIME_FORMAT_PREF for further details of available formats.
userPreferredTimeFormat = if (is24HourFormat) 2 else 1
}
}

// At this point userPreferredTimeFormat will ALWAYS be either 1 or 2 - regardless of if the saved
// pref is 0 to "Follow system settings".
return if (userPreferredTimeFormat == 1) "hh:mm a" else "HH:mm"
}

fun getHourFormat(c: Context?): String {
return if ((DateFormat.is24HourFormat(c))) "HH:mm" else "hh:mm a"
// Method to take the SimpleDateFormat.getPattern() string and set the user's preferred date format appropriately
private fun updateDateFormatSettingsFromPattern(dateFormatPattern: String) {
when (dateFormatPattern) {
"M/d/yy" -> { userPreferredDateFormat = 1; userPreferredDateFormatPattern = dateFormatPattern }
"d/M/yy" -> { userPreferredDateFormat = 2; userPreferredDateFormatPattern = dateFormatPattern }
"dd/MM/yyyy" -> { userPreferredDateFormat = 3; userPreferredDateFormatPattern = dateFormatPattern }
"dd.MM.yyyy" -> { userPreferredDateFormat = 4; userPreferredDateFormatPattern = dateFormatPattern }
"dd-MM-yyyy" -> { userPreferredDateFormat = 5; userPreferredDateFormatPattern = dateFormatPattern }
"yyyy/M/d" -> { userPreferredDateFormat = 6; userPreferredDateFormatPattern = dateFormatPattern }
"yyyy.M.d" -> { userPreferredDateFormat = 7; userPreferredDateFormatPattern = dateFormatPattern }
"yyyy-M-d" -> { userPreferredDateFormat = 8; userPreferredDateFormatPattern = dateFormatPattern }
else -> {
// Sane fallback for unrecognised date format patten
userPreferredDateFormat = 3; userPreferredDateFormatPattern = "dd/MM/yyyy"
}
}
}

// Method to take an int from the user's dateFormatPref and update our date format pattern accordingly
private fun updateDateFormatSettingsFromInt(dateFormatPrefInt: Int) {
var mutableDateFormatPrefInt = dateFormatPrefInt
// There's probably a more elegant way to do this - but we can't use `.also { }` because
// the else block
when (mutableDateFormatPrefInt) {
1 -> { userPreferredDateFormatPattern = "M/d/yy" }
2 -> { userPreferredDateFormatPattern = "d/M/yy" }
3 -> { userPreferredDateFormatPattern = "dd/MM/yyyy" }
4 -> { userPreferredDateFormatPattern = "dd.MM.yyyy" }
5 -> { userPreferredDateFormatPattern = "dd-MM-yyyy" }
6 -> { userPreferredDateFormatPattern = "yyyy/M/d" }
7 -> { userPreferredDateFormatPattern = "yyyy.M.d" }
8 -> { userPreferredDateFormatPattern = "yyyy-M-d" }
else -> {
// Sane fallback for unrecognised date format pattern ("dd/MM/yyyy")
Log.w("DateUtils", "Bad dateFormatPrefInt - falling back to dd/MM/yyyy")
mutableDateFormatPrefInt = 3 // Because we 'also' update our setting from this!
userPreferredDateFormatPattern = "dd/MM/yyyy"
}
}.also {
// Typically we pass in `userPreferredDataFormat` TO this method - but because we perform
// sanitisation in the case of a bad value we'll also write a cleaned version back to the var.
userPreferredDateFormat = mutableDateFormatPrefInt
}
}

private fun isWithinOneWeek(timestamp: Long) = System.currentTimeMillis() - timestamp <= TimeUnit.DAYS.toMillis(7)
private fun isWithinOneYear(timestamp: Long) = System.currentTimeMillis() - timestamp <= TimeUnit.DAYS.toMillis(365)

fun getDisplayFormattedTimeSpanString(c: Context, locale: Locale, timestamp: Long): String {
// If the timestamp is within the last 24 hours we just give the time, e.g, "1:23 PM" or
// "13:23" depending on 12/24 hour formatting.
return if (isToday(timestamp)) {
// Note: Date patterns are in TR-35 format.
// See: https://www.unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns
val t = if (isToday(timestamp)) {
Log.w("ACL", "Within today")
// If it's within the last 24 hours we just give the time in 24-hour format, such as "13:27" for 1:27pm
getFormattedDateTime(timestamp, getHourFormat(c), locale)
} else if (isWithin(timestamp, 6, TimeUnit.DAYS)) {
} else if (isWithinOneWeek(timestamp)) {
Log.w("ACL", "Within week")
// If it's within the last week we give the day as 3 letters then the time in 24-hour format, such as "Fri 13:27" for Friday 1:27pm
getFormattedDateTime(timestamp, "EEE " + getHourFormat(c), locale)
} else if (isWithin(timestamp, 365, TimeUnit.DAYS)) {
} else if (isWithinOneYear(timestamp)) {
Log.w("ACL", "Within year")
// If it's within the last year we give the month as 3 letters then the time in 24-hour format, such as "Mar 13:27" for March 1:27pm
// CAREFUL: "MMM d + getHourFormat(c)" actually turns out to be "8 July, 17:14" etc. - it is DAY-NUMBER and then MONTH (which can go up to 4 chars) - and THEN the time. Wild.
getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c), locale)
} else {
getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale)
// NOTE: The `userPreferredDateFormat` is ONLY ever used on dates which exceed one year!
// See the Figma linked in ticket SES-360 for details.

Log.w("ACL", "More than 1 year")
// If the date is more than a year ago then we get "19 July 2023, 16:19" type format

// If the app has just started..
if (userPreferredDateFormat == -1) {
// ..update our preferred date format (will return -1 if no saved pref).
userPreferredDateFormat = TextSecurePreferences.getDateFormatPref(c)

// If we don't have a saved date format pref we'll write 0 for "Follow system setting".
// Note: This will only execute on first install & run where the pref doesn't exist.
if (userPreferredDateFormat == -1) {
userPreferredDateFormat = 0
TextSecurePreferences.setDateFormatPref(c, userPreferredDateFormat)
}
}

// --- At this point we will always have _some_ preferred date format ---

// If the preferred date format is "Follow system setting" then we need to find out what the system setting is!
if (userPreferredDateFormat == 0) {
val dateFormat = DateFormat.getDateFormat(c)

// Check if the DateFormat instance is a SimpleDateFormat
if (dateFormat is SimpleDateFormat) {
val dateFormatPattern = dateFormat.toLocalizedPattern()
Log.w("ACL", "Date pattern: " + dateFormat.toPattern())

// System setting has a date format pattern? Cool - let's use it, whatever it might be
userPreferredDateFormatPattern = dateFormatPattern

// Update our userPreferredDateFormat & pattern from the system setting
//updateDateFormatSettingsFromPattern(dateFormatPattern)
} else {
// If the dateFormat ISN'T a SimpleDateFormat from which we can extract a pattern then the best
// we can do is pick a sensible default like dd/MM/YYYY - which equates to option 3 out of our
// available options (see TextSecurePreferences.DATE_FORMAT_PREF for further details).
userPreferredDateFormat = 3
userPreferredDateFormatPattern = "dd/MM/yyyy"
}

// IMPORTANT: As we've updated the `userPreferredDataFormat` from "follow system setting" to
// "whatever that system setting is" we DO NOT write that back to the pref - we leave the
// saved value as is so that it always uses that system settings, whatever that may be.
} else {
// If the user has asked for a specific date format that isn't "Follow system setting"
// then update our date formatting settings from that preference.
updateDateFormatSettingsFromInt(userPreferredDateFormat)
}

//userPreferredDateFormatPattern
getFormattedDateTime(timestamp, userPreferredDateFormatPattern, locale)
//getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale)
}

Log.w("ACL", "t is: $t")
return t

}

fun getDetailedDateFormatter(context: Context?, locale: Locale): SimpleDateFormat {
Expand All @@ -127,7 +281,7 @@ object DateUtils : android.text.format.DateUtils() {
} else if (isYesterday(timestamp)) {
getLocalisedRelativeDayString(RelativeDay.YESTERDAY)
} else {
getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale)
getFormattedDateTime(timestamp, userPreferredDateFormatPattern, locale)
}
}

Expand Down
2 changes: 1 addition & 1 deletion app/src/main/res/xml/preferences_privacy.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
android:key="pref_typing_indicators"
android:title="@string/typingIndicators"
android:summary="@string/typingIndicatorsDescription" />
<!-- TODO ACL: Need to show a live typing indicator here & remove the "(...)" from the above string (if it's there - it might have been removed now) -->
<!-- TODO ACL: Need to show a live typing indicator here! -->

</PreferenceCategory>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,26 @@ interface TextSecurePreferences {
// for the lifetime of the Session installation.
const val HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS = "libsession.HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS"

// Key name for the user's preferred date format as an Int. See ticket SES-360 for further details & Figma design.
// Values for various formats are as follows:
// 0 - Follow system settings (default)
// 1 - M/D/YY - example: 1/2/24 (which is 2nd Jan 2024), or 12/25/24 (which is 25th Dec 2024)
// 2 - D/M/YY - example: 2/1/24 (which is 2nd Jan 2024), or 25/12/24 (which is 25th Dec 2024)
// 3 - DD/MM/YYYY - example: 02/01/2024 (which is 2nd Jan 2024), or 25/12/2024 (which is 25th Dec 2024)
// 4 - DD.MM.YYYY - example: 02.01.2024 (which is 2nd Jan 2024), or 25.12.2024 (which is 25th Dec 2024)
// 5 - DD-MM-YYYY - example: 02-01-2024 (which is 2nd Jan 2024), or 25-12-2024 (which is 25th Dec 2024)
// 6 - YYYY/M/D - example: 2024/1/2 (which is 2nd Jan 2024), or 2024/12/25 (which is 25th Dec 2024)
// 7 - YYYY.M.D - example: 2024.1.2 (which is 2nd Jan 2024), or 2024.12.25 (which is 25th Dec 2024)
// 8 - YYYY-M-D - example: 2024-1-2 (which is 2nd Jan 2024), or 2024-12-25 (which is 25th Dec 2024)
const val DATE_FORMAT_PREF = "libsession.DATE_FORMAT_PREF"

// Key name for the user's preferred time format as an Int
// Values for various formats are as follows:
// 0 - Follow system settings (default)
// 1 - 12h - example: 3:45 PM
// 2 - 24h - example: 15:45
const val TIME_FORMAT_PREF = "libsession.TIME_FORMAT_PREF"

@JvmStatic
fun getLastConfigurationSyncTime(context: Context): Long {
return getLongPreference(context, LAST_CONFIGURATION_SYNC_TIME, 0)
Expand Down Expand Up @@ -990,8 +1010,14 @@ interface TextSecurePreferences {
setBooleanPreference(context, FINGERPRINT_KEY_GENERATED, true)
}

@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
}


// ----- Get / set methods for if we have already warned the user that saving attachments will allow other apps to access them -----
// Note: We only ever show the warning dialog about this ONCE - when the user accepts this fact we write true to the flag & never show again.
@JvmStatic
fun getHaveWarnedUserAboutSavingAttachments(context: Context): Boolean {
return getBooleanPreference(context, HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS, false)
Expand All @@ -1001,12 +1027,26 @@ interface TextSecurePreferences {
fun setHaveWarnedUserAboutSavingAttachments(context: Context) {
setBooleanPreference(context, HAVE_WARNED_USER_ABOUT_SAVING_ATTACHMENTS, true)
}
// ---------------------------------------------------------------------------------------------------------------------------------

// ----- Get / set methods for the user's date format preference -----
@JvmStatic
fun clearAll(context: Context) {
getDefaultSharedPreferences(context).edit().clear().commit()
fun getDateFormatPref(context: Context): Int {
// Note: 0 means "follow system setting" (default) - go to the declaration of DATE_FORMAT_PREF for further details.
return getIntegerPreference(context, DATE_FORMAT_PREF, -1)
}

@JvmStatic
fun setDateFormatPref(context: Context, value: Int) { setIntegerPreference(context, DATE_FORMAT_PREF, value) }

// ----- Get / set methods for the user's time format preference -----
@JvmStatic
fun getTimeFormatPref(context: Context): Int {
// Note: 0 means "follow system setting" (default) - go to the declaration of TIME_FORMAT_PREF for further details.
return getIntegerPreference(context, TIME_FORMAT_PREF, -1)
}

@JvmStatic
fun setTimeFormatPref(context: Context, value: Int) { setIntegerPreference(context, TIME_FORMAT_PREF, value) }
}
}

Expand Down