Show running live state item

This commit is contained in:
Maxime NATUREL 2022-05-02 12:18:21 +02:00
parent adbc430ac8
commit 077977b8bf
4 changed files with 88 additions and 20 deletions

View File

@ -16,24 +16,36 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.VectorEpoxyModel
import im.vector.app.core.resources.DateProvider
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.LiveLocationShareSummaryData
import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem
import im.vector.app.features.home.room.detail.timeline.item.MessageLiveLocationStartItem_
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.location.toLocationData
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.threeten.bp.LocalDateTime
import timber.log.Timber
import javax.inject.Inject
class LiveLocationShareMessageItemFactory @Inject constructor(
private val session: Session,
private val dimensionConverter: DimensionConverter,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val avatarSizeProvider: AvatarSizeProvider,
private val urlMapProvider: UrlMapProvider,
private val locationPinProvider: LocationPinProvider,
private val vectorDateFormatter: VectorDateFormatter,
) {
fun create(
@ -41,10 +53,10 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
): VectorEpoxyModel<*>? {
return when (getViewState(liveLocationShareSummaryData)) {
return when (val currentState = getViewState(liveLocationShareSummaryData)) {
LiveLocationShareViewState.Loading -> buildLoadingItem(highlight, attributes)
LiveLocationShareViewState.Inactive -> buildInactiveItem()
is LiveLocationShareViewState.Running -> buildRunningItem()
is LiveLocationShareViewState.Running -> buildRunningItem(highlight, attributes, currentState)
LiveLocationShareViewState.Unkwown -> null
}
}
@ -64,7 +76,32 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
.leftGuideline(avatarSizeProvider.leftGuideline)
}
private fun buildRunningItem() = null
private fun buildRunningItem(
highlight: Boolean,
attributes: AbsMessageItem.Attributes,
runningState: LiveLocationShareViewState.Running,
): MessageLiveLocationItem {
// TODO only render location if enabled in preferences: to be handled in a next PR
val width = timelineMediaSizeProvider.getMaxSize().first
val height = dimensionConverter.dpToPx(MessageItemFactory.MESSAGE_LOCATION_ITEM_HEIGHT_IN_DP)
val locationUrl = runningState.lastGeoUri.toLocationData()?.let {
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
}
return MessageLiveLocationItem_()
.attributes(attributes)
.locationUrl(locationUrl)
.mapWidth(width)
.mapHeight(height)
.locationUserId(attributes.informationData.senderId)
.locationPinProvider(locationPinProvider)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.currentUserId(session.myUserId)
.endOfLiveDateTime(runningState.endOfLiveDateTime)
.vectorDateFormatter(vectorDateFormatter)
}
private fun buildInactiveItem() = null

View File

@ -20,30 +20,42 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R
import im.vector.app.core.date.DateFormatKind
import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.resources.toTimestamp
import im.vector.app.core.utils.DimensionConverter
import im.vector.app.features.home.room.detail.timeline.style.TimelineMessageLayout
import im.vector.app.features.location.live.LocationLiveMessageBannerView
import im.vector.app.features.location.live.LocationLiveMessageBannerViewState
import org.threeten.bp.LocalDateTime
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocationItem.Holder>() {
// TODO define the needed attributes
@EpoxyAttribute
var currentUserId: String? = null
@EpoxyAttribute
var endOfLiveDateTime: LocalDateTime? = null
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
lateinit var vectorDateFormatter: VectorDateFormatter
override fun bind(holder: Holder) {
super.bind(holder)
bindLocationLiveBanner(holder)
}
private fun bindLocationLiveBanner(holder: Holder) {
// TODO add check on device id to confirm that is the one that sent the beacon
// TODO in a future PR add check on device id to confirm that is the one that sent the beacon
val isEmitter = currentUserId != null && currentUserId == locationUserId
val messageLayout = attributes.informationData.messageLayout
val viewState = buildViewState(holder, messageLayout, isEmitter)
holder.locationLiveMessageBanner.isVisible = true
holder.locationLiveMessageBanner.render(viewState)
holder.locationLiveMessageBanner.stopButton.setOnClickListener {
// TODO call stop live location
}
// TODO adjust Copyright map placement if needed
}
@ -55,7 +67,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
return when {
messageLayout is TimelineMessageLayout.Bubble && isEmitter ->
LocationLiveMessageBannerViewState.Emitter(
remainingTimeInMillis = 4000 * 1000L,
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
isStopButtonCenteredVertically = false
@ -64,12 +76,12 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
LocationLiveMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = messageLayout.cornersRadius.bottomStartRadius,
bottomEndCornerRadiusInDp = messageLayout.cornersRadius.bottomEndRadius,
formattedLocalTimeOfEndOfLive = "12:34",
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
)
isEmitter -> {
val cornerRadius = getBannerCornerRadiusForDefaultLayout(holder)
LocationLiveMessageBannerViewState.Emitter(
remainingTimeInMillis = 4000 * 1000L,
remainingTimeInMillis = getRemainingTimeOfLiveInMillis(),
bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius,
isStopButtonCenteredVertically = true
@ -80,7 +92,7 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
LocationLiveMessageBannerViewState.Watcher(
bottomStartCornerRadiusInDp = cornerRadius,
bottomEndCornerRadiusInDp = cornerRadius,
formattedLocalTimeOfEndOfLive = "12:34",
formattedLocalTimeOfEndOfLive = getFormattedLocalTimeEndOfLive(),
)
}
}
@ -91,6 +103,12 @@ abstract class MessageLiveLocationItem : AbsMessageLocationItem<MessageLiveLocat
return dimensionConverter.dpToPx(8).toFloat()
}
private fun getFormattedLocalTimeEndOfLive() =
endOfLiveDateTime?.toTimestamp()?.let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) }.orEmpty()
private fun getRemainingTimeOfLiveInMillis() =
(endOfLiveDateTime?.toTimestamp() ?: 0) - LocalDateTime.now().toTimestamp()
class Holder : AbsMessageLocationItem.Holder() {
val locationLiveMessageBanner by bind<LocationLiveMessageBannerView>(R.id.locationLiveMessageBanner)
}

View File

@ -29,7 +29,7 @@ data class LocationData(
) : Parcelable
/**
* Creates location data from a LocationContent
* Creates location data from a MessageLocationContent
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
* @return location data or null if geo uri is not valid
*/
@ -37,6 +37,15 @@ fun MessageLocationContent.toLocationData(): LocationData? {
return parseGeo(getBestGeoUri())
}
/**
* Creates location data from a geoUri String
* "geo:40.05,29.24;30" -> LocationData(40.05, 29.24, 30)
* @return location data or null if geo uri is null or not valid
*/
fun String?.toLocationData(): LocationData? {
return this?.let { parseGeo(it) }
}
@VisibleForTesting
fun parseGeo(geo: String): LocationData? {
val geoParts = geo

View File

@ -98,17 +98,21 @@ class LocationLiveMessageBannerView @JvmOverloads constructor(
title.text = context.getString(R.string.location_share_live_enabled)
countDownTimer?.cancel()
countDownTimer = object : CountDownTimer(viewState.remainingTimeInMillis, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) {
override fun onTick(millisUntilFinished: Long) {
val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L))
subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration))
}
viewState.remainingTimeInMillis
.takeIf { it >= 0 }
?.let {
countDownTimer = object : CountDownTimer(it, REMAINING_TIME_COUNTER_INTERVAL_IN_MS) {
override fun onTick(millisUntilFinished: Long) {
val duration = Duration.ofMillis(millisUntilFinished.coerceAtLeast(0L))
subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, duration))
}
override fun onFinish() {
subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L)))
}
}
countDownTimer?.start()
override fun onFinish() {
subTitle.text = context.getString(R.string.location_share_live_remaining_time, TextUtils.formatDurationWithUnits(context, Duration.ofMillis(0L)))
}
}
countDownTimer?.start()
}
val rootLayout: ConstraintLayout? = (binding.root as? ConstraintLayout)
rootLayout?.let { parentLayout ->