Show running live state item
This commit is contained in:
parent
adbc430ac8
commit
077977b8bf
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ->
|
||||
|
|
Loading…
Reference in New Issue