Create a UrlMapProvider for a better handling of RTL languages, and build the URLs in the controllers

This commit is contained in:
Benoit Marty 2022-01-28 22:46:58 +01:00
parent eff6942f82
commit 2ce3894562
11 changed files with 102 additions and 70 deletions

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="is_rtl">true</bool>
</resources>

View File

@ -4,4 +4,6 @@
<!-- Created to detect what has to be implemented (especially in the settings) --> <!-- Created to detect what has to be implemented (especially in the settings) -->
<bool name="false_not_implemented">false</bool> <bool name="false_not_implemented">false</bool>
<bool name="is_rtl">false</bool>
</resources> </resources>

View File

@ -36,9 +36,6 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.home.room.detail.timeline.item.BindingOptions import im.vector.app.features.home.room.detail.timeline.item.BindingOptions
import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess import im.vector.app.features.home.room.detail.timeline.tools.findPillsAndProcess
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.getStaticMapUrl
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.EpoxyCharSequence
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@ -74,7 +71,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
var time: String? = null var time: String? = null
@EpoxyAttribute @EpoxyAttribute
var locationData: LocationData? = null var locationUrl: String? = null
@EpoxyAttribute @EpoxyAttribute
var locationPinProvider: LocationPinProvider? = null var locationPinProvider: LocationPinProvider? = null
@ -85,12 +82,6 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
@EpoxyAttribute(EpoxyAttribute.Option.DoNotHash) @EpoxyAttribute(EpoxyAttribute.Option.DoNotHash)
var userClicked: ClickListener? = null var userClicked: ClickListener? = null
@EpoxyAttribute
var mapWidth: Int = 1200
@EpoxyAttribute
var mapHeight: Int = 800
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
avatarRenderer.render(matrixItem, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
@ -107,14 +98,14 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) } body.charSequence.findPillsAndProcess(coroutineScope) { it.bind(holder.body) }
holder.timestamp.setTextOrHide(time) holder.timestamp.setTextOrHide(time)
if (locationData == null) { if (locationUrl == null) {
holder.body.isVisible = true holder.body.isVisible = true
holder.mapViewContainer.isVisible = false holder.mapViewContainer.isVisible = false
} else { } else {
holder.body.isVisible = false holder.body.isVisible = false
holder.mapViewContainer.isVisible = true holder.mapViewContainer.isVisible = true
GlideApp.with(holder.staticMapImageView) GlideApp.with(holder.staticMapImageView)
.load(getStaticMapUrl(locationData!!.latitude, locationData!!.longitude, INITIAL_MAP_ZOOM_IN_TIMELINE, mapWidth, mapHeight)) .load(locationUrl)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(holder.staticMapImageView) .into(holder.staticMapImageView)

View File

@ -39,7 +39,9 @@ import im.vector.app.features.home.room.detail.timeline.item.E2EDecoration
import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovementMethod
import im.vector.app.features.home.room.detail.timeline.tools.linkify import im.vector.app.features.home.room.detail.timeline.tools.linkify
import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.SpanUtils
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence import im.vector.lib.core.utils.epoxy.charsequence.toEpoxyCharSequence
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
@ -62,6 +64,7 @@ class MessageActionsEpoxyController @Inject constructor(
private val spanUtils: SpanUtils, private val spanUtils: SpanUtils,
private val eventDetailsFormatter: EventDetailsFormatter, private val eventDetailsFormatter: EventDetailsFormatter,
private val dateFormatter: VectorDateFormatter, private val dateFormatter: VectorDateFormatter,
private val urlMapProvider: UrlMapProvider,
private val locationPinProvider: LocationPinProvider private val locationPinProvider: LocationPinProvider
) : TypedEpoxyController<MessageActionState>() { ) : TypedEpoxyController<MessageActionState>() {
@ -74,9 +77,11 @@ class MessageActionsEpoxyController @Inject constructor(
val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL) val formattedDate = dateFormatter.format(date, DateFormatKind.MESSAGE_DETAIL)
val body = state.messageBody.linkify(host.listener) val body = state.messageBody.linkify(host.listener)
val bindingOptions = spanUtils.getBindingOptions(body) val bindingOptions = spanUtils.getBindingOptions(body)
val locationData = state.timelineEvent()?.root?.getClearContent()?.toModel<MessageLocationContent>(catchError = true)?.let { val locationUrl = state.timelineEvent()?.root?.getClearContent()
LocationData.create(it.getUri()) ?.toModel<MessageLocationContent>(catchError = true)
} ?.let { LocationData.create(it.getUri()) }
?.let { urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, 1200, 800) }
bottomSheetMessagePreviewItem { bottomSheetMessagePreviewItem {
id("preview") id("preview")
avatarRenderer(host.avatarRenderer) avatarRenderer(host.avatarRenderer)
@ -89,7 +94,7 @@ class MessageActionsEpoxyController @Inject constructor(
body(body.toEpoxyCharSequence()) body(body.toEpoxyCharSequence())
bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence()) bodyDetails(host.eventDetailsFormatter.format(state.timelineEvent()?.root)?.toEpoxyCharSequence())
time(formattedDate) time(formattedDate)
locationData(locationData) locationUrl(locationUrl)
locationPinProvider(host.locationPinProvider) locationPinProvider(host.locationPinProvider)
} }

View File

@ -72,7 +72,9 @@ import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.PillsPostProcessor import im.vector.app.features.html.PillsPostProcessor
import im.vector.app.features.html.SpanUtils import im.vector.app.features.html.SpanUtils
import im.vector.app.features.html.VectorHtmlCompressor import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.LocationData import im.vector.app.features.location.LocationData
import im.vector.app.features.location.UrlMapProvider
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer import im.vector.app.features.media.VideoContentRenderer
import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.settings.VectorPreferences
@ -129,7 +131,9 @@ class MessageItemFactory @Inject constructor(
private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker, private val voiceMessagePlaybackTracker: VoiceMessagePlaybackTracker,
private val locationPinProvider: LocationPinProvider, private val locationPinProvider: LocationPinProvider,
private val vectorPreferences: VectorPreferences, private val vectorPreferences: VectorPreferences,
private val resources: Resources) { private val urlMapProvider: UrlMapProvider,
private val resources: Resources
) {
// TODO inject this properly? // TODO inject this properly?
private var roomId: String = "" private var roomId: String = ""
@ -212,13 +216,15 @@ class MessageItemFactory @Inject constructor(
val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60) val width = resources.displayMetrics.widthPixels - dimensionConverter.dpToPx(60)
val height = dimensionConverter.dpToPx(200) val height = dimensionConverter.dpToPx(200)
val locationUrl = locationData?.let {
urlMapProvider.buildStaticMapUrl(it, INITIAL_MAP_ZOOM_IN_TIMELINE, width, height)
}
return MessageLocationItem_() return MessageLocationItem_()
.attributes(attributes) .attributes(attributes)
.locationData(locationData) .locationUrl(locationUrl)
.userId(informationData.senderId) .userId(informationData.senderId)
.locationPinProvider(locationPinProvider) .locationPinProvider(locationPinProvider)
.mapWidth(width)
.mapHeight(height)
.highlighted(highlight) .highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline) .leftGuideline(avatarSizeProvider.leftGuideline)
.callback(mapCallback) .callback(mapCallback)

View File

@ -24,9 +24,6 @@ import im.vector.app.R
import im.vector.app.core.epoxy.onClick import im.vector.app.core.epoxy.onClick
import im.vector.app.core.glide.GlideApp import im.vector.app.core.glide.GlideApp
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
import im.vector.app.features.location.INITIAL_MAP_ZOOM_IN_TIMELINE
import im.vector.app.features.location.LocationData
import im.vector.app.features.location.getStaticMapUrl
@EpoxyModelClass(layout = R.layout.item_timeline_event_base) @EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() { abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>() {
@ -39,7 +36,7 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
var callback: Callback? = null var callback: Callback? = null
@EpoxyAttribute @EpoxyAttribute
var locationData: LocationData? = null var locationUrl: String? = null
@EpoxyAttribute @EpoxyAttribute
var userId: String? = null var userId: String? = null
@ -47,17 +44,11 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
@EpoxyAttribute @EpoxyAttribute
var locationPinProvider: LocationPinProvider? = null var locationPinProvider: LocationPinProvider? = null
@EpoxyAttribute
var mapWidth: Int = 1200
@EpoxyAttribute
var mapHeight: Int = 800
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
renderSendState(holder.view, null) renderSendState(holder.view, null)
val location = locationData ?: return val location = locationUrl ?: return
val locationOwnerId = userId ?: return val locationOwnerId = userId ?: return
holder.view.onClick { holder.view.onClick {
@ -65,7 +56,7 @@ abstract class MessageLocationItem : AbsMessageItem<MessageLocationItem.Holder>(
} }
GlideApp.with(holder.staticMapImageView) GlideApp.with(holder.staticMapImageView)
.load(getStaticMapUrl(location.latitude, location.longitude, INITIAL_MAP_ZOOM_IN_TIMELINE, mapWidth, mapHeight)) .load(location)
.apply(RequestOptions.centerCropTransform()) .apply(RequestOptions.centerCropTransform())
.into(holder.staticMapImageView) .into(holder.staticMapImageView)

View File

@ -16,34 +16,10 @@
package im.vector.app.features.location package im.vector.app.features.location
import im.vector.app.BuildConfig const val MAP_BASE_URL = "https://api.maptiler.com/maps/streets/style.json"
const val STATIC_MAP_BASE_URL = "https://api.maptiler.com/maps/basic/static/"
const val MAP_STYLE_URL = "https://api.maptiler.com/maps/streets/style.json?key=${BuildConfig.mapTilerKey}"
private const val STATIC_MAP_IMAGE_URL = "https://api.maptiler.com/maps/basic/static/"
const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0 const val INITIAL_MAP_ZOOM_IN_PREVIEW = 15.0
const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0 const val INITIAL_MAP_ZOOM_IN_TIMELINE = 17.0
const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds const val MIN_TIME_TO_UPDATE_LOCATION_MILLIS = 5 * 1_000L // every 5 seconds
const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f const val MIN_DISTANCE_TO_UPDATE_LOCATION_METERS = 10f
fun getStaticMapUrl(latitude: Double,
longitude: Double,
zoom: Double,
width: Int,
height: Int): String {
return buildString {
append(STATIC_MAP_IMAGE_URL)
append(longitude)
append(",")
append(latitude)
append(",")
append(zoom)
append("/")
append(width)
append("x")
append(height)
append(".png?key=")
append(BuildConfig.mapTilerKey)
append("&attribution=bottomleft")
}
}

View File

@ -36,6 +36,7 @@ import javax.inject.Inject
* TODO Move locationPinProvider to a ViewModel * TODO Move locationPinProvider to a ViewModel
*/ */
class LocationPreviewFragment @Inject constructor( class LocationPreviewFragment @Inject constructor(
private val urlMapProvider: UrlMapProvider,
private val locationPinProvider: LocationPinProvider private val locationPinProvider: LocationPinProvider
) : VectorBaseFragment<FragmentLocationPreviewBinding>() { ) : VectorBaseFragment<FragmentLocationPreviewBinding>() {
@ -53,7 +54,7 @@ class LocationPreviewFragment @Inject constructor(
mapView = WeakReference(views.mapView) mapView = WeakReference(views.mapView)
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
views.mapView.initialize() views.mapView.initialize(urlMapProvider.mapUrl)
loadPinDrawable() loadPinDrawable()
} }

View File

@ -35,7 +35,9 @@ import javax.inject.Inject
/** /**
* We should consider using SupportMapFragment for a out of the box lifecycle handling * We should consider using SupportMapFragment for a out of the box lifecycle handling
*/ */
class LocationSharingFragment @Inject constructor() : VectorBaseFragment<FragmentLocationSharingBinding>() { class LocationSharingFragment @Inject constructor(
private val urlMapProvider: UrlMapProvider
) : VectorBaseFragment<FragmentLocationSharingBinding>() {
private val viewModel: LocationSharingViewModel by fragmentViewModel() private val viewModel: LocationSharingViewModel by fragmentViewModel()
@ -51,7 +53,7 @@ class LocationSharingFragment @Inject constructor() : VectorBaseFragment<Fragmen
mapView = WeakReference(views.mapView) mapView = WeakReference(views.mapView)
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
views.mapView.initialize() views.mapView.initialize(urlMapProvider.mapUrl)
views.shareLocationContainer.debouncedClicks { views.shareLocationContainer.debouncedClicks {
viewModel.handle(LocationSharingAction.OnShareLocation) viewModel.handle(LocationSharingAction.OnShareLocation)

View File

@ -27,7 +27,6 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
import com.mapbox.mapboxsdk.style.layers.Property import com.mapbox.mapboxsdk.style.layers.Property
import timber.log.Timber import timber.log.Timber
import java.util.concurrent.atomic.AtomicBoolean
class MapTilerMapView @JvmOverloads constructor( class MapTilerMapView @JvmOverloads constructor(
context: Context, context: Context,
@ -43,21 +42,16 @@ class MapTilerMapView @JvmOverloads constructor(
val style: Style val style: Style
) )
private var isInitializing = AtomicBoolean(false)
private var mapRefs: MapRefs? = null private var mapRefs: MapRefs? = null
private var initZoomDone = false private var initZoomDone = false
/** /**
* For location fragments * For location fragments
*/ */
fun initialize() { fun initialize(url: String) {
Timber.d("## Location: initialize $isInitializing") Timber.d("## Location: initialize")
if (isInitializing.getAndSet(true)) {
return
}
getMapAsync { map -> getMapAsync { map ->
map.setStyle(MAP_STYLE_URL) { style -> map.setStyle(url) { style ->
mapRefs = MapRefs( mapRefs = MapRefs(
map, map,
SymbolManager(this, map, style), SymbolManager(this, map, style),

View File

@ -0,0 +1,58 @@
/*
* 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
import android.content.res.Resources
import im.vector.app.BuildConfig
import im.vector.app.R
import javax.inject.Inject
class UrlMapProvider @Inject constructor(
private val resources: Resources
) {
private val keyParam = "?key=${BuildConfig.mapTilerKey}"
// This is static so no need for a fun
val mapUrl = buildString {
append(MAP_BASE_URL)
append(keyParam)
}
fun buildStaticMapUrl(locationData: LocationData,
zoom: Double,
width: Int,
height: Int): String {
return buildString {
append(STATIC_MAP_BASE_URL)
append(locationData.longitude)
append(",")
append(locationData.latitude)
append(",")
append(zoom)
append("/")
append(width)
append("x")
append(height)
append(".png")
append(keyParam)
if (!resources.getBoolean(R.bool.is_rtl)) {
// On LTR languages we want the legal mentions to be displayed on the bottom left of the image
append("&attribution=bottomleft")
}
}
}
}