Merge pull request #2040 from vector-im/feature/date_formatting

Feature/date formatting
This commit is contained in:
Benoit Marty 2020-09-09 12:16:18 +02:00 committed by GitHub
commit 94e43475e2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 366 additions and 126 deletions

View file

@ -5,7 +5,7 @@ Features ✨:
- -
Improvements 🙌: Improvements 🙌:
- - Handle date formatting properly (show time am/pm if needed, display year when needed)
Bugfix 🐛: Bugfix 🐛:
- -

View file

@ -164,7 +164,7 @@ Formatter\.formatShortFileSize===1
# android\.text\.TextUtils # android\.text\.TextUtils
### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If it is ok, change the value in file forbidden_strings_in_code.txt
enum class===76 enum class===77
### Do not import temporary legacy classes ### Do not import temporary legacy classes
import org.matrix.android.sdk.internal.legacy.riot===3 import org.matrix.android.sdk.internal.legacy.riot===3

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class AbbrevDateFormatterProvider @Inject constructor(private val localeProvider: LocaleProvider) : DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMM")
DateTimeFormatter.ofPattern(pattern, localeProvider.current())
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "dd.MM.yyyy")
DateTimeFormatter.ofPattern(pattern, localeProvider.current())
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
/* This will represent all kind of available date formats for the app.
We will use the date Sep 7 2020 at 9:30am as an example.
The formatting is depending of the current date.
*/
enum class DateFormatKind {
// Will show date relative and time (today or yesterday or Sep 7 or 09/07/2020 at 9:30am)
DEFAULT_DATE_AND_TIME,
// Will show hour or date relative (9:30am or yesterday or Sep 7 or 09/07/2020)
ROOM_LIST,
// Will show full date (Sep 7 2020)
TIMELINE_DAY_DIVIDER,
// Will show full date and time (Mon, Sep 7 2020, 9:30am)
MESSAGE_DETAIL,
// Will only show time (9:30am)
MESSAGE_SIMPLE,
// Will only show time (9:30am)
EDIT_HISTORY_ROW,
// Will only show date relative (today or yesterday or Sep 7 or 09/07/2020)
EDIT_HISTORY_HEADER
}

View file

@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import org.threeten.bp.format.DateTimeFormatter
interface DateFormatterProvider {
val dateWithMonthFormatter: DateTimeFormatter
val dateWithYearFormatter: DateTimeFormatter
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import javax.inject.Inject
class DateFormatterProviders @Inject constructor(private val defaultDateFormatterProvider: DefaultDateFormatterProvider,
private val abbrevDateFormatterProvider: AbbrevDateFormatterProvider) {
fun provide(abbrev: Boolean): DateFormatterProvider {
return if (abbrev) {
abbrevDateFormatterProvider
} else {
defaultDateFormatterProvider
}
}
}

View file

@ -0,0 +1,38 @@
/*
* Copyright (c) 2020 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app.core.date
import android.content.Context
import android.text.format.DateFormat
import im.vector.app.core.resources.LocaleProvider
import org.threeten.bp.format.DateTimeFormatter
import javax.inject.Inject
class DefaultDateFormatterProvider @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider)
: DateFormatterProvider {
override val dateWithMonthFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMMMM")
DateTimeFormatter.ofPattern(pattern)
}
override val dateWithYearFormatter: DateTimeFormatter by lazy {
val pattern = DateFormat.getBestDateTimePattern(localeProvider.current(), "d MMM y")
DateTimeFormatter.ofPattern(pattern)
}
}

View file

@ -19,64 +19,147 @@ package im.vector.app.core.date
import android.content.Context import android.content.Context
import android.text.format.DateFormat import android.text.format.DateFormat
import android.text.format.DateUtils import android.text.format.DateUtils
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.LocaleProvider import im.vector.app.core.resources.LocaleProvider
import im.vector.app.core.resources.toTimestamp
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
import org.threeten.bp.Period
import org.threeten.bp.format.DateTimeFormatter import org.threeten.bp.format.DateTimeFormatter
import java.util.Calendar
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
import kotlin.math.absoluteValue
/**
* Returns the timestamp for the start of the day of the provided time.
* For example, for the time "Jul 21, 11:11" the start of the day: "Jul 21, 00:00" is returned.
*/
fun startOfDay(time: Long): Long {
val calendar = Calendar.getInstance()
calendar.time = Date(time)
calendar.set(Calendar.HOUR_OF_DAY, 0)
calendar.set(Calendar.MINUTE, 0)
calendar.set(Calendar.SECOND, 0)
calendar.set(Calendar.MILLISECOND, 0)
return calendar.time.time
}
class VectorDateFormatter @Inject constructor(private val context: Context, class VectorDateFormatter @Inject constructor(private val context: Context,
private val localeProvider: LocaleProvider) { private val localeProvider: LocaleProvider,
private val dateFormatterProviders: DateFormatterProviders) {
private val messageHourFormatter by lazy { private val hourFormatter by lazy {
DateTimeFormatter.ofPattern("H:mm", localeProvider.current()) if (DateFormat.is24HourFormat(context)) {
DateTimeFormatter.ofPattern("HH:mm", localeProvider.current())
} else {
DateTimeFormatter.ofPattern("h:mm a", localeProvider.current())
}
} }
private val messageDayFormatter by lazy { private val fullDateFormatter by lazy {
DateTimeFormatter.ofPattern(DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE d MMM")) val pattern = if (DateFormat.is24HourFormat(context)) {
} DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE, d MMM yyyy HH:mm")
} else {
fun formatMessageHour(localDateTime: LocalDateTime): String { DateFormat.getBestDateTimePattern(localeProvider.current(), "EEE, d MMM yyyy h:mm a")
return messageHourFormatter.format(localDateTime) }
} DateTimeFormatter.ofPattern(pattern, localeProvider.current())
fun formatMessageDay(localDateTime: LocalDateTime): String {
return messageDayFormatter.format(localDateTime)
} }
/** /**
* Formats a localized relative date time for the last 2 days, e.g, "Today, HH:MM", "Yesterday, HH:MM" or * This method is used to format some date in the app.
* "2 days ago, HH:MM". * It will be able to show only time, only date or both with some logic.
* For earlier timestamps the absolute date time is returned, e.g. "Month Day, HH:MM". * @param ts the timestamp to format or null.
* @param dateFormatKind the kind of format to use
* *
* @param time the absolute timestamp [ms] that should be formatted relative to now * @return the formatted date as string.
*/ */
fun formatRelativeDateTime(time: Long?): String { fun format(ts: Long?, dateFormatKind: DateFormatKind): String {
if (time == null) { if (ts == null) return "-"
val localDateTime = DateProvider.toLocalDateTime(ts)
return when (dateFormatKind) {
DateFormatKind.DEFAULT_DATE_AND_TIME -> formatDateAndTime(ts)
DateFormatKind.ROOM_LIST -> formatTimeOrDate(
date = localDateTime,
showTimeIfSameDay = true,
abbrev = true,
useRelative = true
)
DateFormatKind.TIMELINE_DAY_DIVIDER -> formatTimeOrDate(
date = localDateTime,
alwaysShowYear = true
)
DateFormatKind.MESSAGE_DETAIL -> formatFullDate(localDateTime)
DateFormatKind.MESSAGE_SIMPLE -> formatHour(localDateTime)
DateFormatKind.EDIT_HISTORY_ROW -> formatHour(localDateTime)
DateFormatKind.EDIT_HISTORY_HEADER -> formatTimeOrDate(
date = localDateTime,
abbrev = true,
useRelative = true
)
}
}
private fun formatFullDate(localDateTime: LocalDateTime): String {
return fullDateFormatter.format(localDateTime)
}
private fun formatHour(localDateTime: LocalDateTime): String {
return hourFormatter.format(localDateTime)
}
private fun formatDateWithMonth(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithMonthFormatter.format(localDateTime)
}
private fun formatDateWithYear(localDateTime: LocalDateTime, abbrev: Boolean = false): String {
return dateFormatterProviders.provide(abbrev).dateWithYearFormatter.format(localDateTime)
}
/**
* This method will only show time or date following the parameters.
*/
private fun formatTimeOrDate(
date: LocalDateTime?,
showTimeIfSameDay: Boolean = false,
useRelative: Boolean = false,
alwaysShowYear: Boolean = false,
abbrev: Boolean = false
): String {
if (date == null) {
return "" return ""
} }
val now = System.currentTimeMillis() val currentDate = DateProvider.currentLocalDateTime()
return DateUtils.getRelativeDateTimeString( val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
context, return if (showTimeIfSameDay && isSameDay) {
time, formatHour(date)
} else {
formatDate(date, currentDate, alwaysShowYear, abbrev, useRelative)
}
}
private fun formatDate(
date: LocalDateTime,
currentDate: LocalDateTime,
alwaysShowYear: Boolean,
abbrev: Boolean,
useRelative: Boolean
): String {
val period = Period.between(date.toLocalDate(), currentDate.toLocalDate())
return if (period.years.absoluteValue >= 1 || alwaysShowYear) {
formatDateWithYear(date, abbrev)
} else if (useRelative && period.days.absoluteValue < 2 && period.months.absoluteValue < 1) {
getRelativeDay(date.toTimestamp())
} else {
formatDateWithMonth(date, abbrev)
}
}
/**
* This method will show date and time with a preposition
*/
private fun formatDateAndTime(ts: Long): String {
val date = DateProvider.toLocalDateTime(ts)
val currentDate = DateProvider.currentLocalDateTime()
// This fake date is created to be able to use getRelativeTimeSpanString so we can get a "at"
// preposition and the right am/pm management.
val fakeDate = LocalDateTime.of(currentDate.toLocalDate(), date.toLocalTime())
val formattedTime = DateUtils.getRelativeTimeSpanString(context, fakeDate.toTimestamp(), true).toString()
val formattedDate = formatDate(date, currentDate, alwaysShowYear = false, abbrev = true, useRelative = true)
return "$formattedDate $formattedTime"
}
/**
* We are using this method for the keywords Today/Yesterday
*/
private fun getRelativeDay(ts: Long): String {
return DateUtils.getRelativeTimeSpanString(
ts,
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS, DateUtils.DAY_IN_MILLIS,
now - startOfDay(now - 2 * DateUtils.DAY_IN_MILLIS), DateUtils.FORMAT_SHOW_WEEKDAY).toString()
DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_TIME
).toString()
} }
} }

View file

@ -23,6 +23,10 @@ import org.threeten.bp.ZoneId
object DateProvider { object DateProvider {
private val zoneId = ZoneId.systemDefault() private val zoneId = ZoneId.systemDefault()
private val zoneOffset by lazy {
val now = currentLocalDateTime()
zoneId.rules.getOffset(now)
}
fun toLocalDateTime(timestamp: Long?): LocalDateTime { fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0) val instant = Instant.ofEpochMilli(timestamp ?: 0)
@ -33,4 +37,10 @@ object DateProvider {
val instant = Instant.now() val instant = Instant.now()
return LocalDateTime.ofInstant(instant, zoneId) return LocalDateTime.ofInstant(instant, zoneId)
} }
fun toTimestamp(localDateTime: LocalDateTime): Long {
return localDateTime.toInstant(zoneOffset).toEpochMilli()
}
} }
fun LocalDateTime.toTimestamp(): Long = DateProvider.toTimestamp(this)

View file

@ -21,6 +21,8 @@ package im.vector.app.features.crypto.keysrequest
import android.content.Context import android.content.Context
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.popup.DefaultVectorAlert import im.vector.app.features.popup.DefaultVectorAlert
import im.vector.app.features.popup.PopupAlertManager import im.vector.app.features.popup.PopupAlertManager
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
@ -38,10 +40,6 @@ import org.matrix.android.sdk.internal.crypto.model.CryptoDeviceInfo
import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap import org.matrix.android.sdk.internal.crypto.model.MXUsersDevicesMap
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -54,8 +52,11 @@ import javax.inject.Singleton
*/ */
@Singleton @Singleton
class KeyRequestHandler @Inject constructor(private val context: Context, private val popupAlertManager: PopupAlertManager) class KeyRequestHandler @Inject constructor(
: GossipingRequestListener, private val context: Context,
private val popupAlertManager: PopupAlertManager,
private val dateFormatter: VectorDateFormatter
) : GossipingRequestListener,
VerificationService.Listener { VerificationService.Listener {
private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>() private val alertsToRequests = HashMap<String, ArrayList<IncomingRoomKeyRequest>>()
@ -156,16 +157,7 @@ class KeyRequestHandler @Inject constructor(private val context: Context, privat
moreInfo.lastSeenIp moreInfo.lastSeenIp
} }
val lastSeenTime = moreInfo.lastSeenTs?.let { ts -> val lastSeenTime = dateFormatter.format(moreInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
val lastSeenInfo = context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) val lastSeenInfo = context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)
dialogText = if (wasNewDevice) { dialogText = if (wasNewDevice) {
context.getString(R.string.you_added_a_new_device_with_info, deviceName, lastSeenInfo) context.getString(R.string.you_added_a_new_device_with_info, deviceName, lastSeenInfo)

View file

@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.readreceipts package im.vector.app.features.home.room.detail.readreceipts
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
@ -36,7 +37,7 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte
override fun buildModels(readReceipts: List<ReadReceiptData>) { override fun buildModels(readReceipts: List<ReadReceiptData>) {
readReceipts.forEach { readReceipts.forEach {
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) val timestamp = dateFormatter.format(it.timestamp, DateFormatKind.DEFAULT_DATE_AND_TIME)
DisplayReadReceiptItem_() DisplayReadReceiptItem_()
.id(it.userId) .id(it.userId)
.matrixItem(it.toMatrixItem()) .matrixItem(it.toMatrixItem())

View file

@ -25,6 +25,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
@ -53,7 +54,6 @@ import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoCon
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
import org.matrix.android.sdk.api.session.room.timeline.Timeline import org.matrix.android.sdk.api.session.room.timeline.Timeline
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.threeten.bp.LocalDateTime
import javax.inject.Inject import javax.inject.Inject
class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter, class TimelineEventController @Inject constructor(private val dateFormatter: VectorDateFormatter,
@ -333,13 +333,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
) { ) {
requestModelBuild() requestModelBuild()
} }
val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, date) val daySeparatorItem = buildDaySeparatorItem(addDaySeparator, event.root.originServerTs)
return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem) return CacheItemData(event.localId, event.root.eventId, eventModel, mergedHeaderModel, daySeparatorItem)
} }
private fun buildDaySeparatorItem(addDaySeparator: Boolean, date: LocalDateTime): DaySeparatorItem? { private fun buildDaySeparatorItem(addDaySeparator: Boolean, originServerTs: Long?): DaySeparatorItem? {
return if (addDaySeparator) { return if (addDaySeparator) {
val formattedDay = dateFormatter.formatMessageDay(date) val formattedDay = dateFormatter.format(originServerTs, DateFormatKind.TIMELINE_DAY_DIVIDER)
DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay) DaySeparatorItem_().formattedDay(formattedDay).id(formattedDay)
} else { } else {
null null

View file

@ -22,9 +22,6 @@ import com.airbnb.mvrx.Uninitialized
import im.vector.app.core.extensions.canReact import im.vector.app.core.extensions.canReact
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/** /**
* Quick reactions state * Quick reactions state
@ -56,11 +53,7 @@ data class MessageActionState(
constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData) constructor(args: TimelineEventFragmentArgs) : this(roomId = args.roomId, eventId = args.eventId, informationData = args.informationData)
private val dateFormat = SimpleDateFormat("EEE, d MMM yyyy HH:mm", Locale.getDefault())
fun senderName(): String = informationData.memberName?.toString() ?: "" fun senderName(): String = informationData.memberName?.toString() ?: ""
fun time(): String? = timelineEvent()?.root?.originServerTs?.let { dateFormat.format(Date(it)) } ?: ""
fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact fun canReact() = timelineEvent()?.canReact() == true && actionPermissions.canReact
} }

View file

@ -20,6 +20,8 @@ import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.EmojiCompatFontProvider import im.vector.app.EmojiCompatFontProvider
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem import im.vector.app.core.epoxy.bottomsheet.BottomSheetQuickReactionsItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetActionItem
import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
@ -40,13 +42,16 @@ import javax.inject.Inject
class MessageActionsEpoxyController @Inject constructor( class MessageActionsEpoxyController @Inject constructor(
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val fontProvider: EmojiCompatFontProvider private val fontProvider: EmojiCompatFontProvider,
private val dateFormatter: VectorDateFormatter
) : TypedEpoxyController<MessageActionState>() { ) : TypedEpoxyController<MessageActionState>() {
var listener: MessageActionsEpoxyControllerListener? = null var listener: MessageActionsEpoxyControllerListener? = null
override fun buildModels(state: MessageActionState) { override fun buildModels(state: MessageActionState) {
// Message preview // Message preview
val date = state.timelineEvent()?.root?.originServerTs
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
bottomSheetMessagePreviewItem { bottomSheetMessagePreviewItem {
id("preview") id("preview")
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
@ -54,7 +59,7 @@ class MessageActionsEpoxyController @Inject constructor(
movementMethod(createLinkMovementMethod(listener)) movementMethod(createLinkMovementMethod(listener))
userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) } userClicked { listener?.didSelectMenuAction(EventSharedAction.OpenUserProfile(state.informationData.senderId)) }
body(state.messageBody.linkify(listener)) body(state.messageBody.linkify(listener))
time(state.time()) time(formattedDate)
} }
// Send state // Send state

View file

@ -17,27 +17,26 @@ package im.vector.app.features.home.room.detail.timeline.edithistory
import android.content.Context import android.content.Context
import android.text.Spannable import android.text.Spannable
import android.text.format.DateUtils
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Incomplete import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
import im.vector.app.core.ui.list.genericItem import im.vector.app.core.ui.list.genericItem
import im.vector.app.core.ui.list.genericItemHeader import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.core.ui.list.genericLoaderItem import im.vector.app.core.ui.list.genericLoaderItem
import im.vector.app.features.html.EventHtmlRenderer import im.vector.app.features.html.EventHtmlRenderer
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent import org.matrix.android.sdk.api.session.room.model.message.MessageTextContent
import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply import org.matrix.android.sdk.api.util.ContentUtils.extractUsefulTextFromReply
import org.matrix.android.sdk.internal.session.room.send.TextContent import org.matrix.android.sdk.internal.session.room.send.TextContent
import me.gujun.android.span.span
import name.fraser.neil.plaintext.diff_match_patch
import java.util.Calendar import java.util.Calendar
/** /**
@ -82,11 +81,9 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) { if (lastDate?.get(Calendar.DAY_OF_YEAR) != evDate.get(Calendar.DAY_OF_YEAR)) {
// need to display header with day // need to display header with day
val dateString = if (DateUtils.isToday(evDate.timeInMillis)) context.getString(R.string.today)
else dateFormatter.formatMessageDay(timelineEvent.localDateTime())
genericItemHeader { genericItemHeader {
id(evDate.hashCode()) id(evDate.hashCode())
text(dateString) text(dateFormatter.format(evDate.timeInMillis, DateFormatKind.EDIT_HISTORY_HEADER))
} }
} }
lastDate = evDate lastDate = evDate
@ -130,7 +127,7 @@ class ViewEditHistoryEpoxyController(private val context: Context,
} }
genericItem { genericItem {
id(timelineEvent.eventId) id(timelineEvent.eventId)
title(dateFormatter.formatMessageHour(timelineEvent.localDateTime())) title(dateFormatter.format(timelineEvent.originServerTs, DateFormatKind.EDIT_HISTORY_ROW))
description(spannedDiff ?: body) description(spannedDiff ?: body)
} }
} }

View file

@ -18,6 +18,7 @@
package im.vector.app.features.home.room.detail.timeline.helper package im.vector.app.features.home.room.detail.timeline.helper
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
@ -68,7 +69,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|| isNextMessageReceivedMoreThanOneHourAgo || isNextMessageReceivedMoreThanOneHourAgo
|| isTileTypeMessage(nextEvent) || isTileTypeMessage(nextEvent)
val time = dateFormatter.formatMessageHour(date) val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
val e2eDecoration = getE2EDecoration(event) val e2eDecoration = getE2EDecoration(event)
return MessageInformationData( return MessageInformationData(

View file

@ -24,17 +24,18 @@ import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.platform.EmptyAction import im.vector.app.core.platform.EmptyAction
import im.vector.app.core.platform.EmptyViewEvents import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs import im.vector.app.features.home.room.detail.timeline.action.TimelineEventFragmentArgs
import io.reactivex.Observable
import io.reactivex.Single
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary import org.matrix.android.sdk.api.session.room.model.ReactionAggregatedSummary
import org.matrix.android.sdk.rx.RxRoom import org.matrix.android.sdk.rx.RxRoom
import org.matrix.android.sdk.rx.unwrap import org.matrix.android.sdk.rx.unwrap
import io.reactivex.Observable
import io.reactivex.Single
data class DisplayReactionsViewState( data class DisplayReactionsViewState(
val eventId: String, val eventId: String,
@ -112,7 +113,7 @@ class ViewReactionsViewModel @AssistedInject constructor(@Assisted
summary.key, summary.key,
event.root.senderId ?: "", event.root.senderId ?: "",
event.senderInfo.disambiguatedDisplayName, event.senderInfo.disambiguatedDisplayName,
dateFormatter.formatRelativeDateTime(event.root.originServerTs) dateFormatter.format(event.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
) )
} }

View file

@ -18,10 +18,9 @@ package im.vector.app.features.home.room.list
import android.view.View import android.view.View
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.VectorEpoxyModel import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.DebouncedClickListener import im.vector.app.core.utils.DebouncedClickListener
import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.home.AvatarRenderer
@ -53,8 +52,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
} }
private fun createInvitationItem(roomSummary: RoomSummary, private fun createInvitationItem(roomSummary: RoomSummary,
changeMembershipState: ChangeMembershipState, changeMembershipState: ChangeMembershipState,
listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> { listener: RoomSummaryController.Listener?): VectorEpoxyModel<*> {
val secondLine = if (roomSummary.isDirect) { val secondLine = if (roomSummary.isDirect) {
roomSummary.inviterId roomSummary.inviterId
} else { } else {
@ -87,15 +86,8 @@ class RoomSummaryItemFactory @Inject constructor(private val displayableEventFor
var latestEventTime: CharSequence = "" var latestEventTime: CharSequence = ""
val latestEvent = roomSummary.latestPreviewableEvent val latestEvent = roomSummary.latestPreviewableEvent
if (latestEvent != null) { if (latestEvent != null) {
val date = latestEvent.root.localDateTime()
val currentDate = DateProvider.currentLocalDateTime()
val isSameDay = date.toLocalDate() == currentDate.toLocalDate()
latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not()) latestFormattedEvent = displayableEventFormatter.format(latestEvent, roomSummary.isDirect.not())
latestEventTime = if (isSameDay) { latestEventTime = dateFormatter.format(latestEvent.root.originServerTs, DateFormatKind.ROOM_LIST)
dateFormatter.formatMessageHour(date)
} else {
dateFormatter.formatMessageDay(date)
}
} }
val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers) val typingMessage = typingHelper.getTypingMessage(roomSummary.typingUsers)
return RoomSummaryItem_() return RoomSummaryItem_()

View file

@ -19,8 +19,8 @@ package im.vector.app.features.media
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime
import im.vector.lib.attachmentviewer.AttachmentInfo import im.vector.lib.attachmentviewer.AttachmentInfo
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.events.model.isVideoMessage import org.matrix.android.sdk.api.session.events.model.isVideoMessage
@ -78,9 +78,7 @@ class DataAttachmentRoomProvider(
val item = attachments[position] val item = attachments[position]
val timeLineEvent = room?.getTimeLineEvent(item.eventId) val timeLineEvent = room?.getTimeLineEvent(item.eventId)
if (timeLineEvent != null) { if (timeLineEvent != null) {
val dateString = timeLineEvent.root.localDateTime().let { val dateString = dateFormatter.format(timeLineEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
}
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString") overlayView?.updateWith("${position + 1} of ${attachments.size}", "${timeLineEvent.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage() overlayView?.videoControlsGroup?.isVisible = timeLineEvent.root.isVideoMessage()
} else { } else {

View file

@ -19,8 +19,8 @@ package im.vector.app.features.media
import android.content.Context import android.content.Context
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.extensions.localDateTime
import im.vector.lib.attachmentviewer.AttachmentInfo import im.vector.lib.attachmentviewer.AttachmentInfo
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -128,9 +128,7 @@ class RoomEventsAttachmentProvider(
override fun overlayViewAtPosition(context: Context, position: Int): View? { override fun overlayViewAtPosition(context: Context, position: Int): View? {
super.overlayViewAtPosition(context, position) super.overlayViewAtPosition(context, position)
val item = attachments[position] val item = attachments[position]
val dateString = item.root.localDateTime().let { val dateString = dateFormatter.format(item.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
"${dateFormatter.formatMessageDay(it)} at ${dateFormatter.formatMessageHour(it)} "
}
overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString") overlayView?.updateWith("${position + 1} of ${attachments.size}", "${item.senderInfo.displayName} $dateString")
overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage() overlayView?.videoControlsGroup?.isVisible = item.root.isVideoMessage()
return overlayView return overlayView

View file

@ -18,12 +18,13 @@ package im.vector.app.features.roomprofile.uploads.files
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.epoxy.VisibilityState import com.airbnb.epoxy.VisibilityState
import org.matrix.android.sdk.api.session.room.uploads.UploadEvent
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState import im.vector.app.features.roomprofile.uploads.RoomUploadsViewState
import org.matrix.android.sdk.api.session.room.uploads.UploadEvent
import javax.inject.Inject import javax.inject.Inject
class UploadsFileController @Inject constructor( class UploadsFileController @Inject constructor(
@ -71,7 +72,7 @@ class UploadsFileController @Inject constructor(
title(uploadEvent.contentWithAttachmentContent.body) title(uploadEvent.contentWithAttachmentContent.body)
subtitle(stringProvider.getString(R.string.uploads_files_subtitle, subtitle(stringProvider.getString(R.string.uploads_files_subtitle,
uploadEvent.senderInfo.disambiguatedDisplayName, uploadEvent.senderInfo.disambiguatedDisplayName,
dateFormatter.formatRelativeDateTime(uploadEvent.root.originServerTs))) dateFormatter.format(uploadEvent.root.originServerTs, DateFormatKind.DEFAULT_DATE_AND_TIME)))
listener(object : UploadsFileItem.Listener { listener(object : UploadsFileItem.Listener {
override fun onItemClicked() { override fun onItemClicked() {
listener?.onOpenClicked(uploadEvent) listener?.onOpenClicked(uploadEvent)

View file

@ -31,10 +31,6 @@ import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import me.gujun.android.span.span import me.gujun.android.span.span
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/** /**
* A list item for Device. * A list item for Device.
@ -45,6 +41,9 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
@EpoxyAttribute @EpoxyAttribute
lateinit var deviceInfo: DeviceInfo lateinit var deviceInfo: DeviceInfo
@EpoxyAttribute
var lastSeenFormatted: String? = null
@EpoxyAttribute @EpoxyAttribute
var currentDevice = false var currentDevice = false
@ -105,15 +104,7 @@ abstract class DeviceItem : VectorEpoxyModel<DeviceItem.Holder>() {
val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-" val lastSeenIp = deviceInfo.lastSeenIp?.takeIf { ip -> ip.isNotBlank() } ?: "-"
val lastSeenTime = deviceInfo.lastSeenTs?.let { ts -> val lastSeenTime = lastSeenFormatted ?: "-"
val dateFormatTime = SimpleDateFormat("HH:mm:ss", Locale.ROOT)
val date = Date(ts)
val time = dateFormatTime.format(date)
val dateFormat = DateFormat.getDateInstance(DateFormat.SHORT, Locale.getDefault())
dateFormat.format(date) + ", " + time
} ?: "-"
holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime) holder.deviceLastSeenText.text = holder.root.context.getString(R.string.devices_details_last_seen_format, lastSeenIp, lastSeenTime)

View file

@ -21,9 +21,9 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.errorWithRetryItem import im.vector.app.core.epoxy.errorWithRetryItem
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.error.ErrorFormatter import im.vector.app.core.error.ErrorFormatter
@ -32,11 +32,14 @@ import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.genericItemHeader import im.vector.app.core.ui.list.genericItemHeader
import im.vector.app.core.utils.DimensionConverter import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.internal.crypto.crosssigning.DeviceTrustLevel
import org.matrix.android.sdk.internal.crypto.model.rest.DeviceInfo
import javax.inject.Inject import javax.inject.Inject
class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter, class DevicesController @Inject constructor(private val errorFormatter: ErrorFormatter,
private val stringProvider: StringProvider, private val stringProvider: StringProvider,
private val colorProvider: ColorProvider, private val colorProvider: ColorProvider,
private val dateFormatter: VectorDateFormatter,
private val dimensionConverter: DimensionConverter, private val dimensionConverter: DimensionConverter,
private val vectorPreferences: VectorPreferences) : EpoxyController() { private val vectorPreferences: VectorPreferences) : EpoxyController() {
@ -100,6 +103,7 @@ class DevicesController @Inject constructor(private val errorFormatter: ErrorFor
deviceInfo(deviceInfo) deviceInfo(deviceInfo)
currentDevice(true) currentDevice(true)
e2eCapable(true) e2eCapable(true)
lastSeenFormatted(dateFormatter.format(deviceInfo.lastSeenTs, DateFormatKind.DEFAULT_DATE_AND_TIME))
itemClickAction { callback?.onDeviceClicked(deviceInfo) } itemClickAction { callback?.onDeviceClicked(deviceInfo) }
trusted(DeviceTrustLevel(currentSessionCrossTrusted, true)) trusted(DeviceTrustLevel(currentSessionCrossTrusted, true))
} }

View file

@ -31,11 +31,11 @@ import org.matrix.android.sdk.internal.crypto.model.rest.GossipingToDeviceObject
import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest import org.matrix.android.sdk.internal.crypto.model.rest.RoomKeyShareRequest
import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest import org.matrix.android.sdk.internal.crypto.model.rest.SecretShareRequest
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.loadingItem import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.ui.list.GenericItem import im.vector.app.core.ui.list.GenericItem
import im.vector.app.core.ui.list.genericFooterItem import im.vector.app.core.ui.list.genericFooterItem
@ -94,8 +94,7 @@ class GossipingEventsEpoxyController @Inject constructor(
) )
description( description(
span { span {
+vectorDateFormatter.formatMessageDay(DateProvider.toLocalDateTime(event.ageLocalTs)) +vectorDateFormatter.format(event.ageLocalTs, DateFormatKind.DEFAULT_DATE_AND_TIME)
+" ${vectorDateFormatter.formatMessageHour(DateProvider.toLocalDateTime(event.ageLocalTs))}"
span("\nfrom: ") { span("\nfrom: ") {
textStyle = "bold" textStyle = "bold"
} }