Merge pull request #6170 from vector-im/feature/ons/live_location_bottom_sheet
Live Location Sharing - User List Bottom Sheet [PSF-890]
This commit is contained in:
commit
4ccd242cbf
|
@ -0,0 +1 @@
|
|||
Live Location Sharing - User List Bottom Sheet
|
|
@ -18,4 +18,26 @@
|
|||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Body.BottomSheetDisplayName">
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Body.BottomSheetRemainingTime">
|
||||
<item name="android:textSize">12sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Vector.Body.BottomSheetLastUpdatedAt">
|
||||
<item name="android:textSize">12sp</item>
|
||||
<item name="android:textColor">?vctr_content_tertiary</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Vector.Button.Text.BottomSheetStopSharing">
|
||||
<item name="android:foreground">?selectableItemBackground</item>
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
<item name="android:textAppearance">@style/TextAppearance.Vector.Body.Medium</item>
|
||||
<item name="android:textColor">?colorError</item>
|
||||
<item name="android:padding">0dp</item>
|
||||
<item name="android:gravity">center</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"data": [
|
||||
{
|
||||
"displayName": "Amandine",
|
||||
"remainingTime": "9min left",
|
||||
"lastUpdatedAt": "Updated 12min ago"
|
||||
},
|
||||
{
|
||||
"displayName": "You",
|
||||
"remainingTime": "19min left",
|
||||
"lastUpdatedAt": "Updated 1min ago"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -20,6 +20,7 @@ import android.content.Context
|
|||
import android.os.Build
|
||||
import android.text.format.Formatter
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import org.threeten.bp.Duration
|
||||
import java.util.TreeMap
|
||||
|
||||
|
@ -85,50 +86,66 @@ object TextUtils {
|
|||
}
|
||||
}
|
||||
|
||||
fun formatDurationWithUnits(context: Context, duration: Duration): String {
|
||||
fun formatDurationWithUnits(context: Context, duration: Duration, appendSeconds: Boolean = true): String {
|
||||
return formatDurationWithUnits(duration, context::getString, appendSeconds)
|
||||
}
|
||||
|
||||
fun formatDurationWithUnits(stringProvider: StringProvider, duration: Duration, appendSeconds: Boolean = true): String {
|
||||
return formatDurationWithUnits(duration, stringProvider::getString, appendSeconds)
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't always have Context to get strings or we want to use StringProvider instead.
|
||||
* So we can pass the getString function either from Context or the StringProvider.
|
||||
* @param duration duration to be formatted
|
||||
* @param getString getString method from Context or StringProvider
|
||||
* @param appendSeconds if false than formatter will not append seconds
|
||||
* @return formatted duration with a localized form like "10h 30min 5sec"
|
||||
*/
|
||||
private fun formatDurationWithUnits(duration: Duration, getString: ((Int) -> String), appendSeconds: Boolean = true): String {
|
||||
val hours = getHours(duration)
|
||||
val minutes = getMinutes(duration)
|
||||
val seconds = getSeconds(duration)
|
||||
val builder = StringBuilder()
|
||||
when {
|
||||
hours > 0 -> {
|
||||
appendHours(context, builder, hours)
|
||||
appendHours(getString, builder, hours)
|
||||
if (minutes > 0) {
|
||||
builder.append(" ")
|
||||
appendMinutes(context, builder, minutes)
|
||||
appendMinutes(getString, builder, minutes)
|
||||
}
|
||||
if (seconds > 0) {
|
||||
if (appendSeconds && seconds > 0) {
|
||||
builder.append(" ")
|
||||
appendSeconds(context, builder, seconds)
|
||||
appendSeconds(getString, builder, seconds)
|
||||
}
|
||||
}
|
||||
minutes > 0 -> {
|
||||
appendMinutes(context, builder, minutes)
|
||||
if (seconds > 0) {
|
||||
appendMinutes(getString, builder, minutes)
|
||||
if (appendSeconds && seconds > 0) {
|
||||
builder.append(" ")
|
||||
appendSeconds(context, builder, seconds)
|
||||
appendSeconds(getString, builder, seconds)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
appendSeconds(context, builder, seconds)
|
||||
appendSeconds(getString, builder, seconds)
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
private fun appendHours(context: Context, builder: StringBuilder, hours: Int) {
|
||||
private fun appendHours(getString: ((Int) -> String), builder: StringBuilder, hours: Int) {
|
||||
builder.append(hours)
|
||||
builder.append(context.resources.getString(R.string.time_unit_hour_short))
|
||||
builder.append(getString(R.string.time_unit_hour_short))
|
||||
}
|
||||
|
||||
private fun appendMinutes(context: Context, builder: StringBuilder, minutes: Int) {
|
||||
private fun appendMinutes(getString: ((Int) -> String), builder: StringBuilder, minutes: Int) {
|
||||
builder.append(minutes)
|
||||
builder.append(context.getString(R.string.time_unit_minute_short))
|
||||
builder.append(getString(R.string.time_unit_minute_short))
|
||||
}
|
||||
|
||||
private fun appendSeconds(context: Context, builder: StringBuilder, seconds: Int) {
|
||||
private fun appendSeconds(getString: ((Int) -> String), builder: StringBuilder, seconds: Int) {
|
||||
builder.append(seconds)
|
||||
builder.append(context.getString(R.string.time_unit_second_short))
|
||||
builder.append(getString(R.string.time_unit_second_short))
|
||||
}
|
||||
|
||||
private fun getHours(duration: Duration): Int = duration.toHours().toInt()
|
||||
|
|
|
@ -22,10 +22,15 @@ import com.mapbox.mapboxsdk.geometry.LatLng
|
|||
import com.mapbox.mapboxsdk.geometry.LatLngBounds
|
||||
import com.mapbox.mapboxsdk.maps.MapboxMap
|
||||
|
||||
fun MapboxMap?.zoomToLocation(locationData: LocationData) {
|
||||
fun MapboxMap?.zoomToLocation(locationData: LocationData, preserveCurrentZoomLevel: Boolean = false) {
|
||||
val zoomLevel = if (preserveCurrentZoomLevel && this?.cameraPosition != null) {
|
||||
cameraPosition.zoom
|
||||
} else {
|
||||
INITIAL_MAP_ZOOM_IN_PREVIEW
|
||||
}
|
||||
this?.cameraPosition = CameraPosition.Builder()
|
||||
.target(LatLng(locationData.latitude, locationData.longitude))
|
||||
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
||||
.zoom(zoomLevel)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.features.location.live.map
|
||||
|
||||
import android.content.Context
|
||||
import com.airbnb.epoxy.EpoxyController
|
||||
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.DateProvider
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.resources.toTimestamp
|
||||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.app.features.location.live.map.bottomsheet.LiveLocationUserItem
|
||||
import im.vector.app.features.location.live.map.bottomsheet.liveLocationUserItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class LiveLocationBottomSheetController @Inject constructor(
|
||||
private val avatarRenderer: AvatarRenderer,
|
||||
private val vectorDateFormatter: VectorDateFormatter,
|
||||
private val stringProvider: StringProvider,
|
||||
private val clock: Clock,
|
||||
private val context: Context,
|
||||
) : EpoxyController() {
|
||||
|
||||
interface Callback {
|
||||
fun onUserSelected(userId: String)
|
||||
fun onStopLocationClicked()
|
||||
}
|
||||
|
||||
private var userLocations: List<UserLiveLocationViewState>? = null
|
||||
var callback: Callback? = null
|
||||
|
||||
fun setData(userLocations: List<UserLiveLocationViewState>) {
|
||||
this.userLocations = userLocations
|
||||
requestModelBuild()
|
||||
}
|
||||
|
||||
override fun buildModels() {
|
||||
val currentUserLocations = userLocations ?: return
|
||||
val host = this
|
||||
|
||||
val userItemCallback = object : LiveLocationUserItem.Callback {
|
||||
override fun onUserSelected(userId: String) {
|
||||
host.callback?.onUserSelected(userId)
|
||||
}
|
||||
|
||||
override fun onStopSharingClicked() {
|
||||
host.callback?.onStopLocationClicked()
|
||||
}
|
||||
}
|
||||
|
||||
currentUserLocations.forEach { liveLocationViewState ->
|
||||
val remainingTime = getFormattedLocalTimeEndOfLive(liveLocationViewState.endOfLiveTimestampMillis)
|
||||
liveLocationUserItem {
|
||||
id(liveLocationViewState.matrixItem.id)
|
||||
callback(userItemCallback)
|
||||
matrixItem(liveLocationViewState.matrixItem)
|
||||
stringProvider(host.stringProvider)
|
||||
clock(host.clock)
|
||||
avatarRenderer(host.avatarRenderer)
|
||||
remainingTime(remainingTime)
|
||||
locationUpdateTimeMillis(liveLocationViewState.locationTimestampMillis)
|
||||
showStopSharingButton(liveLocationViewState.showStopSharingButton)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getFormattedLocalTimeEndOfLive(endOfLiveDateTimestampMillis: Long?): String {
|
||||
val endOfLiveDateTime = DateProvider.toLocalDateTime(endOfLiveDateTimestampMillis)
|
||||
val formattedDateTime = endOfLiveDateTime.toTimestamp().let { vectorDateFormatter.format(it, DateFormatKind.MESSAGE_SIMPLE) }
|
||||
return stringProvider.getString(R.string.location_share_live_until, formattedDateTime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright (c) 2022 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.features.location.live.map.bottomsheet
|
||||
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
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.epoxy.VectorEpoxyHolder
|
||||
import im.vector.app.core.epoxy.VectorEpoxyModel
|
||||
import im.vector.app.core.epoxy.onClick
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.time.Clock
|
||||
import im.vector.app.core.utils.TextUtils
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
import im.vector.lib.core.utils.timer.CountUpTimer
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.threeten.bp.Duration
|
||||
|
||||
@EpoxyModelClass(layout = R.layout.item_live_location_users_bottom_sheet)
|
||||
abstract class LiveLocationUserItem : VectorEpoxyModel<LiveLocationUserItem.Holder>() {
|
||||
|
||||
interface Callback {
|
||||
fun onUserSelected(userId: String)
|
||||
fun onStopSharingClicked()
|
||||
}
|
||||
|
||||
@EpoxyAttribute
|
||||
var callback: Callback? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var matrixItem: MatrixItem
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var avatarRenderer: AvatarRenderer
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var stringProvider: StringProvider
|
||||
|
||||
@EpoxyAttribute
|
||||
lateinit var clock: Clock
|
||||
|
||||
@EpoxyAttribute
|
||||
var remainingTime: String? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var locationUpdateTimeMillis: Long? = null
|
||||
|
||||
@EpoxyAttribute
|
||||
var showStopSharingButton: Boolean = false
|
||||
|
||||
override fun bind(holder: Holder) {
|
||||
super.bind(holder)
|
||||
avatarRenderer.render(matrixItem, holder.itemUserAvatarImageView)
|
||||
holder.itemUserDisplayNameTextView.text = matrixItem.displayName
|
||||
holder.itemRemainingTimeTextView.text = remainingTime
|
||||
|
||||
holder.itemStopSharingButton.isVisible = showStopSharingButton
|
||||
if (showStopSharingButton) {
|
||||
holder.itemStopSharingButton.onClick {
|
||||
callback?.onStopSharingClicked()
|
||||
}
|
||||
}
|
||||
|
||||
stopTimer(holder)
|
||||
holder.timer = CountUpTimer(1000).apply {
|
||||
tickListener = object : CountUpTimer.TickListener {
|
||||
override fun onTick(milliseconds: Long) {
|
||||
holder.itemLastUpdatedAtTextView.text = getFormattedLastUpdatedAt(locationUpdateTimeMillis)
|
||||
}
|
||||
}
|
||||
resume()
|
||||
}
|
||||
|
||||
holder.view.setOnClickListener { callback?.onUserSelected(matrixItem.id) }
|
||||
}
|
||||
|
||||
override fun unbind(holder: Holder) {
|
||||
super.unbind(holder)
|
||||
stopTimer(holder)
|
||||
}
|
||||
|
||||
private fun stopTimer(holder: Holder) {
|
||||
holder.timer?.stop()
|
||||
holder.timer = null
|
||||
}
|
||||
|
||||
private fun getFormattedLastUpdatedAt(locationUpdateTimeMillis: Long?): String {
|
||||
if (locationUpdateTimeMillis == null) return ""
|
||||
val elapsedTime = clock.epochMillis() - locationUpdateTimeMillis
|
||||
val duration = Duration.ofMillis(elapsedTime.coerceAtLeast(0L))
|
||||
val formattedDuration = TextUtils.formatDurationWithUnits(stringProvider, duration, appendSeconds = false)
|
||||
return stringProvider.getString(R.string.live_location_bottom_sheet_last_updated_at, formattedDuration)
|
||||
}
|
||||
|
||||
class Holder : VectorEpoxyHolder() {
|
||||
var timer: CountUpTimer? = null
|
||||
val itemUserAvatarImageView by bind<ImageView>(R.id.itemUserAvatarImageView)
|
||||
val itemUserDisplayNameTextView by bind<TextView>(R.id.itemUserDisplayNameTextView)
|
||||
val itemRemainingTimeTextView by bind<TextView>(R.id.itemRemainingTimeTextView)
|
||||
val itemLastUpdatedAtTextView by bind<TextView>(R.id.itemLastUpdatedAtTextView)
|
||||
val itemStopSharingButton by bind<Button>(R.id.itemStopSharingButton)
|
||||
}
|
||||
}
|
|
@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewModelAction
|
|||
sealed class LocationLiveMapAction : VectorViewModelAction {
|
||||
data class AddMapSymbol(val key: String, val value: Long) : LocationLiveMapAction()
|
||||
data class RemoveMapSymbol(val key: String) : LocationLiveMapAction()
|
||||
object StopSharing : LocationLiveMapAction()
|
||||
}
|
||||
|
|
|
@ -17,7 +17,9 @@
|
|||
package im.vector.app.features.location.live.map
|
||||
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
@ -36,8 +38,9 @@ import com.mapbox.mapboxsdk.style.layers.Property
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.addChildFragment
|
||||
import im.vector.app.core.extensions.configureWith
|
||||
import im.vector.app.core.platform.VectorBaseFragment
|
||||
import im.vector.app.databinding.FragmentSimpleContainerBinding
|
||||
import im.vector.app.databinding.FragmentLocationLiveMapViewBinding
|
||||
import im.vector.app.features.location.UrlMapProvider
|
||||
import im.vector.app.features.location.zoomToBounds
|
||||
import im.vector.app.features.location.zoomToLocation
|
||||
|
@ -49,10 +52,12 @@ import javax.inject.Inject
|
|||
/**
|
||||
* Screen showing a map with all the current users sharing their live location in a room.
|
||||
*/
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<FragmentSimpleContainerBinding>() {
|
||||
class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<FragmentLocationLiveMapViewBinding>() {
|
||||
|
||||
@Inject lateinit var urlMapProvider: UrlMapProvider
|
||||
@Inject lateinit var bottomSheetController: LiveLocationBottomSheetController
|
||||
|
||||
private val viewModel: LocationLiveMapViewModel by fragmentViewModel()
|
||||
|
||||
|
@ -62,8 +67,23 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
private val pendingLiveLocations = mutableListOf<UserLiveLocationViewState>()
|
||||
private var isMapFirstUpdate = true
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentSimpleContainerBinding {
|
||||
return FragmentSimpleContainerBinding.inflate(layoutInflater, container, false)
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentLocationLiveMapViewBinding {
|
||||
return FragmentLocationLiveMapViewBinding.inflate(layoutInflater, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
views.liveLocationBottomSheetRecyclerView.configureWith(bottomSheetController, hasFixedSize = false, disableItemAnimation = true)
|
||||
|
||||
bottomSheetController.callback = object : LiveLocationBottomSheetController.Callback {
|
||||
override fun onUserSelected(userId: String) {
|
||||
handleBottomSheetUserSelected(userId)
|
||||
}
|
||||
|
||||
override fun onStopLocationClicked() {
|
||||
viewModel.handle(LocationLiveMapAction.StopSharing)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
@ -78,7 +98,9 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
mapboxMap.setStyle(urlMapProvider.getMapUrl()) { style ->
|
||||
mapStyle = style
|
||||
this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap)
|
||||
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style)
|
||||
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style).apply {
|
||||
iconAllowOverlap = true
|
||||
}
|
||||
pendingLiveLocations
|
||||
.takeUnless { it.isEmpty() }
|
||||
?.let { updateMap(it) }
|
||||
|
@ -92,11 +114,16 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
?: run {
|
||||
val options = MapboxMapOptions.createFromAttributes(requireContext(), null)
|
||||
SupportMapFragment.newInstance(options)
|
||||
.also { addChildFragment(R.id.fragmentContainer, it, tag = MAP_FRAGMENT_TAG) }
|
||||
.also { addChildFragment(R.id.liveLocationMapFragmentContainer, it, tag = MAP_FRAGMENT_TAG) }
|
||||
}
|
||||
|
||||
override fun invalidate() = withState(viewModel) { viewState ->
|
||||
updateMap(viewState.userLocations)
|
||||
updateUserListBottomSheet(viewState.userLocations)
|
||||
}
|
||||
|
||||
private fun updateUserListBottomSheet(userLocations: List<UserLiveLocationViewState>) {
|
||||
bottomSheetController.setData(userLocations)
|
||||
}
|
||||
|
||||
private fun updateMap(userLiveLocations: List<UserLiveLocationViewState>) {
|
||||
|
@ -116,7 +143,7 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
}
|
||||
|
||||
private fun createOrUpdateSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||
val symbolId = state.mapSymbolIds[userLocation.userId]
|
||||
val symbolId = state.mapSymbolIds[userLocation.matrixItem.id]
|
||||
|
||||
if (symbolId == null || symbolManager.annotations.get(symbolId) == null) {
|
||||
createSymbol(userLocation, symbolManager)
|
||||
|
@ -126,10 +153,10 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
}
|
||||
|
||||
private fun createSymbol(userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||
addUserPinToMapStyle(userLocation.userId, userLocation.pinDrawable)
|
||||
addUserPinToMapStyle(userLocation.matrixItem.id, userLocation.pinDrawable)
|
||||
val symbolOptions = buildSymbolOptions(userLocation)
|
||||
val symbol = symbolManager.create(symbolOptions)
|
||||
viewModel.handle(LocationLiveMapAction.AddMapSymbol(userLocation.userId, symbol.id))
|
||||
viewModel.handle(LocationLiveMapAction.AddMapSymbol(userLocation.matrixItem.id, symbol.id))
|
||||
}
|
||||
|
||||
private fun updateSymbol(symbolId: Long, userLocation: UserLiveLocationViewState, symbolManager: SymbolManager) {
|
||||
|
@ -142,7 +169,7 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
}
|
||||
|
||||
private fun removeOutdatedSymbols(userLiveLocations: List<UserLiveLocationViewState>, symbolManager: SymbolManager) = withState(viewModel) { state ->
|
||||
val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.userId }.toSet())
|
||||
val userIdsToRemove = state.mapSymbolIds.keys.subtract(userLiveLocations.map { it.matrixItem.id }.toSet())
|
||||
userIdsToRemove.forEach { userId ->
|
||||
removeUserPinFromMapStyle(userId)
|
||||
viewModel.handle(LocationLiveMapAction.RemoveMapSymbol(userId))
|
||||
|
@ -187,9 +214,18 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||
private fun buildSymbolOptions(userLiveLocation: UserLiveLocationViewState) =
|
||||
SymbolOptions()
|
||||
.withLatLng(LatLng(userLiveLocation.locationData.latitude, userLiveLocation.locationData.longitude))
|
||||
.withIconImage(userLiveLocation.userId)
|
||||
.withIconImage(userLiveLocation.matrixItem.id)
|
||||
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
||||
|
||||
private fun handleBottomSheetUserSelected(userId: String) = withState(viewModel) { state ->
|
||||
state.userLocations
|
||||
.find { it.matrixItem.id == userId }
|
||||
?.locationData
|
||||
?.let { locationData ->
|
||||
mapboxMap?.get()?.zoomToLocation(locationData, preserveCurrentZoomLevel = true)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAP_FRAGMENT_TAG = "im.vector.app.features.location.live.map"
|
||||
}
|
||||
|
|
|
@ -23,13 +23,15 @@ import dagger.assisted.AssistedInject
|
|||
import im.vector.app.core.di.MavericksAssistedViewModelFactory
|
||||
import im.vector.app.core.di.hiltMavericksViewModelFactory
|
||||
import im.vector.app.core.platform.VectorViewModel
|
||||
import im.vector.app.features.location.LocationSharingServiceConnection
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class LocationLiveMapViewModel @AssistedInject constructor(
|
||||
@Assisted private val initialState: LocationLiveMapViewState,
|
||||
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase
|
||||
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState) {
|
||||
getListOfUserLiveLocationUseCase: GetListOfUserLiveLocationUseCase,
|
||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||
) : VectorViewModel<LocationLiveMapViewState, LocationLiveMapAction, LocationLiveMapViewEvents>(initialState), LocationSharingServiceConnection.Callback {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory : MavericksAssistedViewModelFactory<LocationLiveMapViewModel, LocationLiveMapViewState> {
|
||||
|
@ -42,12 +44,14 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
|||
getListOfUserLiveLocationUseCase.execute(initialState.roomId)
|
||||
.onEach { setState { copy(userLocations = it) } }
|
||||
.launchIn(viewModelScope)
|
||||
locationSharingServiceConnection.bind(this)
|
||||
}
|
||||
|
||||
override fun handle(action: LocationLiveMapAction) {
|
||||
when (action) {
|
||||
is LocationLiveMapAction.AddMapSymbol -> handleAddMapSymbol(action)
|
||||
is LocationLiveMapAction.RemoveMapSymbol -> handleRemoveMapSymbol(action)
|
||||
LocationLiveMapAction.StopSharing -> handleStopSharing()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,4 +68,16 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
|||
copy(mapSymbolIds = newMapSymbolIds)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStopSharing() {
|
||||
locationSharingServiceConnection.stopLiveLocationSharing(initialState.roomId)
|
||||
}
|
||||
|
||||
override fun onLocationServiceRunning() {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
override fun onLocationServiceStopped() {
|
||||
// NOOP
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package im.vector.app.features.location.live.map
|
|||
import android.graphics.drawable.Drawable
|
||||
import com.airbnb.mvrx.MavericksState
|
||||
import im.vector.app.features.location.LocationData
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
data class LocationLiveMapViewState(
|
||||
val roomId: String,
|
||||
|
@ -34,8 +35,10 @@ data class LocationLiveMapViewState(
|
|||
}
|
||||
|
||||
data class UserLiveLocationViewState(
|
||||
val userId: String,
|
||||
val matrixItem: MatrixItem,
|
||||
val pinDrawable: Drawable,
|
||||
val locationData: LocationData,
|
||||
val endOfLiveTimestampMillis: Long?
|
||||
val endOfLiveTimestampMillis: Long?,
|
||||
val locationTimestampMillis: Long?,
|
||||
val showStopSharingButton: Boolean
|
||||
)
|
||||
|
|
|
@ -16,14 +16,18 @@
|
|||
|
||||
package im.vector.app.features.location.live.map
|
||||
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||
import im.vector.app.features.location.toLocationData
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import org.matrix.android.sdk.api.session.getUser
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
import javax.inject.Inject
|
||||
|
||||
class UserLiveLocationViewStateMapper @Inject constructor(
|
||||
private val locationPinProvider: LocationPinProvider,
|
||||
private val activeSessionHolder: ActiveSessionHolder,
|
||||
) {
|
||||
|
||||
suspend fun map(liveLocationShareAggregatedSummary: LiveLocationShareAggregatedSummary) =
|
||||
|
@ -40,11 +44,16 @@ class UserLiveLocationViewStateMapper @Inject constructor(
|
|||
}
|
||||
else -> {
|
||||
locationPinProvider.create(userId) { pinDrawable ->
|
||||
val session = activeSessionHolder.getActiveSession()
|
||||
session.getUser(userId)?.toMatrixItem()?.let { matrixItem ->
|
||||
val locationTimestampMillis = liveLocationShareAggregatedSummary.lastLocationDataContent?.getBestTimestampMillis()
|
||||
val viewState = UserLiveLocationViewState(
|
||||
userId = userId,
|
||||
matrixItem = matrixItem,
|
||||
pinDrawable = pinDrawable,
|
||||
locationData = locationData,
|
||||
endOfLiveTimestampMillis = liveLocationShareAggregatedSummary.endOfLiveTimestampMillis
|
||||
endOfLiveTimestampMillis = liveLocationShareAggregatedSummary.endOfLiveTimestampMillis,
|
||||
locationTimestampMillis = locationTimestampMillis,
|
||||
showStopSharingButton = userId == session.myUserId
|
||||
)
|
||||
continuation.resume(viewState) {
|
||||
// do nothing on cancellation
|
||||
|
@ -53,4 +62,5 @@ class UserLiveLocationViewStateMapper @Inject constructor(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners
|
||||
android:topLeftRadius="16dp"
|
||||
android:topRightRadius="16dp" />
|
||||
<solid android:color="?android:colorBackground" />
|
||||
</shape>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<size android:width="36dp" android:height="6dp" />
|
||||
<solid android:color="?vctr_content_quinary" />
|
||||
<corners android:radius="3dp" />
|
||||
</shape>
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_live_location_users_bottom_sheet">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/liveLocationMapFragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/bottomSheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/bg_live_location_users_bottom_sheet"
|
||||
app:behavior_peekHeight="200dp"
|
||||
app:behavior_hideable="false"
|
||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<View
|
||||
android:id="@+id/liveLocationMapBottomSheetHandle"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="6dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:background="@drawable/ic_bottom_sheet_handle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/liveLocationBottomSheetRecyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/liveLocationMapBottomSheetHandle"
|
||||
tools:listitem="@layout/item_live_location_users_bottom_sheet" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,5 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragmentContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -0,0 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="80dp"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/itemUserAvatarImageView"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@sample/user_round_avatars" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemUserDisplayNameTextView"
|
||||
style="@style/TextAppearance.Vector.Body.BottomSheetDisplayName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintStart_toEndOf="@id/itemUserAvatarImageView"
|
||||
app:layout_constraintTop_toTopOf="@id/itemUserAvatarImageView"
|
||||
tools:text="@sample/live_location_users.json/data/displayName" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemRemainingTimeTextView"
|
||||
style="@style/TextAppearance.Vector.Body.BottomSheetRemainingTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
app:layout_constraintStart_toStartOf="@id/itemUserDisplayNameTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemUserDisplayNameTextView"
|
||||
tools:text="@sample/live_location_users.json/data/remainingTime" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/itemLastUpdatedAtTextView"
|
||||
style="@style/TextAppearance.Vector.Body.BottomSheetLastUpdatedAt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
app:layout_constraintStart_toStartOf="@id/itemRemainingTimeTextView"
|
||||
app:layout_constraintTop_toBottomOf="@id/itemRemainingTimeTextView"
|
||||
tools:text="@sample/live_location_users.json/data/lastUpdatedAt" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/itemStopSharingButton"
|
||||
style="@style/Widget.Vector.Button.Text.BottomSheetStopSharing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/live_location_bottom_sheet_stop_sharing"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -3037,6 +3037,8 @@
|
|||
<string name="live_location_sharing_notification_description">Location sharing is in progress</string>
|
||||
<string name="labs_enable_live_location">Enable Live Location Sharing</string>
|
||||
<string name="labs_enable_live_location_summary">Temporary implementation: locations persist in room history</string>
|
||||
<string name="live_location_bottom_sheet_stop_sharing">Stop sharing</string>
|
||||
<string name="live_location_bottom_sheet_last_updated_at">Updated %1$s ago</string>
|
||||
|
||||
<string name="message_bubbles">Show Message bubbles</string>
|
||||
|
||||
|
|
|
@ -35,6 +35,7 @@ import org.junit.Rule
|
|||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
class GetListOfUserLiveLocationUseCaseTest {
|
||||
|
||||
|
@ -88,16 +89,20 @@ class GetListOfUserLiveLocationUseCaseTest {
|
|||
every { liveData.asFlow() } returns flowOf(summaries)
|
||||
|
||||
val viewState1 = UserLiveLocationViewState(
|
||||
userId = "userId1",
|
||||
matrixItem = MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
|
||||
pinDrawable = mockk(),
|
||||
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||
endOfLiveTimestampMillis = 123
|
||||
endOfLiveTimestampMillis = 123,
|
||||
locationTimestampMillis = 123,
|
||||
showStopSharingButton = false
|
||||
)
|
||||
val viewState2 = UserLiveLocationViewState(
|
||||
userId = "userId2",
|
||||
matrixItem = MatrixItem.UserItem(id = "@userId2:matrix.org", displayName = "User 2", avatarUrl = ""),
|
||||
pinDrawable = mockk(),
|
||||
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||
endOfLiveTimestampMillis = 1234
|
||||
endOfLiveTimestampMillis = 1234,
|
||||
locationTimestampMillis = 1234,
|
||||
showStopSharingButton = false
|
||||
)
|
||||
coEvery { viewStateMapper.map(summary1) } returns viewState1
|
||||
coEvery { viewStateMapper.map(summary2) } returns viewState2
|
||||
|
|
|
@ -18,13 +18,18 @@ package im.vector.app.features.location.live.map
|
|||
|
||||
import com.airbnb.mvrx.test.MvRxTestRule
|
||||
import im.vector.app.features.location.LocationData
|
||||
import im.vector.app.features.location.LocationSharingServiceConnection
|
||||
import im.vector.app.test.test
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
|
||||
class LocationLiveMapViewModelTest {
|
||||
|
||||
|
@ -36,11 +41,13 @@ class LocationLiveMapViewModelTest {
|
|||
private val args = LocationLiveMapViewArgs(roomId = fakeRoomId)
|
||||
|
||||
private val getListOfUserLiveLocationUseCase = mockk<GetListOfUserLiveLocationUseCase>()
|
||||
private val locationServiceConnection = mockk<LocationSharingServiceConnection>()
|
||||
|
||||
private fun createViewModel(): LocationLiveMapViewModel {
|
||||
return LocationLiveMapViewModel(
|
||||
LocationLiveMapViewState(args),
|
||||
getListOfUserLiveLocationUseCase
|
||||
getListOfUserLiveLocationUseCase,
|
||||
locationServiceConnection
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -48,13 +55,15 @@ class LocationLiveMapViewModelTest {
|
|||
fun `given the viewModel has been initialized then viewState contains user locations list`() = runTest {
|
||||
val userLocations = listOf(
|
||||
UserLiveLocationViewState(
|
||||
userId = "",
|
||||
MatrixItem.UserItem(id = "@userId1:matrix.org", displayName = "User 1", avatarUrl = ""),
|
||||
pinDrawable = mockk(),
|
||||
locationData = LocationData(latitude = 1.0, longitude = 2.0, uncertainty = null),
|
||||
endOfLiveTimestampMillis = 123
|
||||
endOfLiveTimestampMillis = 123,
|
||||
locationTimestampMillis = 123,
|
||||
showStopSharingButton = false
|
||||
)
|
||||
)
|
||||
|
||||
every { locationServiceConnection.bind(any()) } just runs
|
||||
every { getListOfUserLiveLocationUseCase.execute(fakeRoomId) } returns flowOf(userLocations)
|
||||
|
||||
val viewModel = createViewModel()
|
||||
|
@ -66,5 +75,7 @@ class LocationLiveMapViewModelTest {
|
|||
)
|
||||
)
|
||||
.finish()
|
||||
|
||||
verify { locationServiceConnection.bind(viewModel) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,18 +18,31 @@ package im.vector.app.features.location.live.map
|
|||
|
||||
import android.graphics.drawable.Drawable
|
||||
import im.vector.app.features.location.LocationData
|
||||
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||
import im.vector.app.test.fakes.FakeLocationPinProvider
|
||||
import im.vector.app.test.fakes.FakeSession
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.getUser
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.session.room.model.message.LocationInfo
|
||||
import org.matrix.android.sdk.api.session.room.model.message.MessageBeaconLocationDataContent
|
||||
import org.matrix.android.sdk.api.session.user.model.User
|
||||
import org.matrix.android.sdk.api.util.MatrixItem
|
||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||
|
||||
private const val A_USER_ID = "aUserId"
|
||||
private const val A_USER_ID = "@aUserId:matrix.org"
|
||||
private const val A_USER_DISPLAY_NAME = "A_USER_DISPLAY_NAME"
|
||||
private const val A_IS_ACTIVE = true
|
||||
private const val A_END_OF_LIVE_TIMESTAMP = 123L
|
||||
private const val A_LOCATION_TIMESTAMP = 122L
|
||||
private const val A_LATITUDE = 40.05
|
||||
private const val A_LONGITUDE = 29.24
|
||||
private const val A_UNCERTAINTY = 30.0
|
||||
|
@ -38,8 +51,23 @@ private const val A_GEO_URI = "geo:$A_LATITUDE,$A_LONGITUDE;$A_UNCERTAINTY"
|
|||
class UserLiveLocationViewStateMapperTest {
|
||||
|
||||
private val locationPinProvider = FakeLocationPinProvider()
|
||||
private val fakeSession = FakeSession()
|
||||
private val fakeActiveSessionHolder = FakeActiveSessionHolder(fakeSession)
|
||||
|
||||
private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance)
|
||||
private val userLiveLocationViewStateMapper = UserLiveLocationViewStateMapper(locationPinProvider.instance, fakeActiveSessionHolder.instance)
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic("org.matrix.android.sdk.api.util.MatrixItemKt")
|
||||
val fakeUser = mockk<User>()
|
||||
every { fakeSession.getUser(A_USER_ID) } returns fakeUser
|
||||
every { fakeUser.toMatrixItem() } returns MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = "")
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
unmockkStatic("org.matrix.android.sdk.api.util.MatrixItemKt")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given a summary with invalid data then result is null`() = runTest {
|
||||
|
@ -66,7 +94,8 @@ class UserLiveLocationViewStateMapperTest {
|
|||
val pinDrawable = mockk<Drawable>()
|
||||
|
||||
val locationDataContent = MessageBeaconLocationDataContent(
|
||||
locationInfo = LocationInfo(geoUri = A_GEO_URI)
|
||||
locationInfo = LocationInfo(geoUri = A_GEO_URI),
|
||||
unstableTimestampMillis = A_LOCATION_TIMESTAMP
|
||||
)
|
||||
val summary = LiveLocationShareAggregatedSummary(
|
||||
userId = A_USER_ID,
|
||||
|
@ -79,14 +108,16 @@ class UserLiveLocationViewStateMapperTest {
|
|||
val viewState = userLiveLocationViewStateMapper.map(summary)
|
||||
|
||||
val expectedViewState = UserLiveLocationViewState(
|
||||
userId = A_USER_ID,
|
||||
matrixItem = MatrixItem.UserItem(id = A_USER_ID, displayName = A_USER_DISPLAY_NAME, avatarUrl = ""),
|
||||
pinDrawable = pinDrawable,
|
||||
locationData = LocationData(
|
||||
latitude = A_LATITUDE,
|
||||
longitude = A_LONGITUDE,
|
||||
uncertainty = A_UNCERTAINTY
|
||||
),
|
||||
endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP
|
||||
endOfLiveTimestampMillis = A_END_OF_LIVE_TIMESTAMP,
|
||||
locationTimestampMillis = A_LOCATION_TIMESTAMP,
|
||||
showStopSharingButton = false
|
||||
)
|
||||
viewState shouldBeEqualTo expectedViewState
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue