Merge branch 'develop' into feature/aris/threads_analytics

This commit is contained in:
ariskotsomitopoulos 2022-03-16 13:19:08 +01:00
commit eee1ec1423
52 changed files with 961 additions and 104 deletions

1
changelog.d/4780.bugfix Normal file
View File

@ -0,0 +1 @@
Poll system notifications on Android are not user friendly

1
changelog.d/5389.wip Normal file
View File

@ -0,0 +1 @@
Introduces FTUE personalisation complete screen along with confetti celebration

1
changelog.d/5417.feature Normal file
View File

@ -0,0 +1 @@
Add ability to pin a location on map for sharing

1
changelog.d/5521.bugfix Normal file
View File

@ -0,0 +1 @@
Fix mentions using matrix.to rather than client defined permalink base url

View File

@ -64,4 +64,7 @@
<!-- Location sharing --> <!-- Location sharing -->
<dimen name="location_sharing_option_default_padding">10dp</dimen> <dimen name="location_sharing_option_default_padding">10dp</dimen>
<dimen name="location_sharing_locate_button_margin_vertical">16dp</dimen>
<dimen name="location_sharing_locate_button_margin_horizontal">12dp</dimen>
<dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen>
</resources> </resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MapTilerMapView">
<attr name="showLocateButton" format="boolean" />
</declare-styleable>
</resources>

View File

@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
applicationFlavor = "TestFlavor", applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider() roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
) )
)) ),
TestPermalinkService()
)
) )
@Test @Test

View File

@ -0,0 +1,51 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.session.room.send
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
class TestPermalinkService : PermalinkService {
override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
return null
}
override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
return ""
}
override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
return ""
}
override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
return null
}
override fun getLinkedId(url: String): String? {
return null
}
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
return when (type) {
HTML -> "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
}
}
}

View File

@ -410,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
fun Event.getPollContent(): MessagePollContent? { fun Event.getPollContent(): MessagePollContent? {
return content.toModel<MessagePollContent>() return content.toModel<MessagePollContent>()
} }
fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START

View File

@ -28,6 +28,11 @@ interface PermalinkService {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/" const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
} }
enum class SpanTemplateType {
HTML,
MARKDOWN
}
/** /**
* Creates a permalink for an event. * Creates a permalink for an event.
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org" * Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
@ -80,4 +85,15 @@ interface PermalinkService {
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink * @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
*/ */
fun getLinkedId(url: String): String? fun getLinkedId(url: String): String?
/**
* Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
* Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
*
* @param type: type of template to create
* @param forceMatrixTo whether we should force using matrix.to base URL
*
* @return the created template
*/
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
} }

View File

@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class LocationAsset( data class LocationAsset(
@Json(name = "type") val type: LocationAssetType? = null @Json(name = "type") val type: String? = null
) )

View File

@ -16,11 +16,20 @@
package org.matrix.android.sdk.api.session.room.model.message package org.matrix.android.sdk.api.session.room.model.message
import com.squareup.moshi.Json /**
import com.squareup.moshi.JsonClass * Define what particular asset is being referred to.
* We don't use enum type since it is not limited to a specific set of values.
* The way this type should be interpreted in client side is described in
* [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
*/
object LocationAssetType {
/**
* Used for user location sharing.
**/
const val SELF = "m.self"
@JsonClass(generateAdapter = false) /**
enum class LocationAssetType { * Used for pin drop location sharing.
@Json(name = "m.self") **/
SELF const val PIN = "m.pin"
} }

View File

@ -42,7 +42,7 @@ data class MessageLocationContent(
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null, @Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
@Json(name = "m.new_content") override val newContent: Content? = null, @Json(name = "m.new_content") override val newContent: Content? = null,
/** /**
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md * See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
*/ */
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null, @Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
@Json(name = "m.location") val locationInfo: LocationInfo? = null, @Json(name = "m.location") val locationInfo: LocationInfo? = null,
@ -54,10 +54,11 @@ data class MessageLocationContent(
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null, @Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
@Json(name = "m.text") val text: String? = null, @Json(name = "m.text") val text: String? = null,
/** /**
* m.asset defines a generic asset that can be used for location tracking but also in other places like * Defines a generic asset that can be used for location tracking but also in other places like
* inventories, geofencing, checkins/checkouts etc. * inventories, geofencing, checkins/checkouts etc.
* It should contain a mandatory namespaced type key defining what particular asset is being referred to. * It should contain a mandatory namespaced type key defining what particular asset is being referred to.
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid. * For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
* See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
*/ */
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null, @Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
@Json(name = "m.asset") val locationAsset: LocationAsset? = null @Json(name = "m.asset") val locationAsset: LocationAsset? = null

View File

@ -142,8 +142,9 @@ interface SendService {
* @param latitude required latitude of the location * @param latitude required latitude of the location
* @param longitude required longitude of the location * @param longitude required longitude of the location
* @param uncertainty Accuracy of the location in meters * @param uncertainty Accuracy of the location in meters
* @param isUserLocation indicates whether the location data corresponds to the user location or not
*/ */
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
/** /**
* Remove this failed message from the timeline * Remove this failed message from the timeline

View File

@ -97,6 +97,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
if (filters.filterEdits) { if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE) not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
} }
if (filters.filterRedacted) { if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED) not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)

View File

@ -26,6 +26,7 @@ internal object TimelineEventFilter {
internal object Content { internal object Content {
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}""" internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}""" internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
} }
/** /**

View File

@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
override fun getLinkedId(url: String): String? { override fun getLinkedId(url: String): String? {
return permalinkFactory.getLinkedId(url) return permalinkFactory.getLinkedId(url)
} }
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
}
} }

View File

@ -21,7 +21,10 @@ import org.matrix.android.sdk.api.MatrixPatterns
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.permalinks.PermalinkData import org.matrix.android.sdk.api.session.permalinks.PermalinkData
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
import org.matrix.android.sdk.internal.di.UserId import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject import javax.inject.Inject
@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
?.substringBeforeLast("?") ?.substringBeforeLast("?")
} }
fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
return buildString {
when (type) {
HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN)
MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_BEGIN)
}
append(baseUrl(forceMatrixTo))
if (useClientFormat(forceMatrixTo)) {
append(USER_PATH)
}
when (type) {
HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_END)
MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_END)
}
}
}
/** /**
* Escape '/' in id, because it is used as a separator * Escape '/' in id, because it is used as a separator
* *
@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
private const val ROOM_PATH = "room/" private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/" private const val USER_PATH = "user/"
private const val GROUP_PATH = "group/" private const val GROUP_PATH = "group/"
private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "<a href=\""
private const val MENTION_SPAN_TO_HTML_TEMPLATE_END = "%1\$s\">%2\$s</a>"
private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
} }
} }

View File

@ -128,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) } .let { sendEvent(it) }
} }
override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable { override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty) return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
.also { createLocalEcho(it) } .also { createLocalEcho(it) }
.let { sendEvent(it) } .let { sendEvent(it) }
} }

View File

@ -227,13 +227,15 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createLocationEvent(roomId: String, fun createLocationEvent(roomId: String,
latitude: Double, latitude: Double,
longitude: Double, longitude: Double,
uncertainty: Double?): Event { uncertainty: Double?,
isUserLocation: Boolean): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty) val geoUri = buildGeoUri(latitude, longitude, uncertainty)
val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
val content = MessageLocationContent( val content = MessageLocationContent(
geoUri = geoUri, geoUri = geoUri,
body = geoUri, body = geoUri,
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri), unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF), unstableLocationAsset = LocationAsset(type = assetType),
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()), unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
unstableText = geoUri unstableText = geoUri
) )

View File

@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.send.pills package org.matrix.android.sdk.internal.session.room.send.pills
import android.text.SpannableString import android.text.SpannableString
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
@ -28,7 +29,8 @@ import javax.inject.Inject
*/ */
internal class TextPillsUtils @Inject constructor( internal class TextPillsUtils @Inject constructor(
private val mentionLinkSpecComparator: MentionLinkSpecComparator, private val mentionLinkSpecComparator: MentionLinkSpecComparator,
private val displayNameResolver: DisplayNameResolver private val displayNameResolver: DisplayNameResolver,
private val permalinkService: PermalinkService
) { ) {
/** /**
@ -36,7 +38,7 @@ internal class TextPillsUtils @Inject constructor(
* @return the transformed String or null if no Span found * @return the transformed String or null if no Span found
*/ */
fun processSpecialSpansToHtml(text: CharSequence): String? { fun processSpecialSpansToHtml(text: CharSequence): String? {
return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE) return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.HTML))
} }
/** /**
@ -44,7 +46,7 @@ internal class TextPillsUtils @Inject constructor(
* @return the transformed String or null if no Span found * @return the transformed String or null if no Span found
*/ */
fun processSpecialSpansToMarkdown(text: CharSequence): String? { fun processSpecialSpansToMarkdown(text: CharSequence): String? {
return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE) return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.MARKDOWN))
} }
private fun transformPills(text: CharSequence, template: String): String? { private fun transformPills(text: CharSequence, template: String): String? {
@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
i++ i++
} }
} }
companion object {
private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
}
} }

View File

@ -0,0 +1,51 @@
/*
* 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.core.animations
import android.content.Context
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import im.vector.app.R
import nl.dionsegijn.konfetti.KonfettiView
import nl.dionsegijn.konfetti.models.Shape
import nl.dionsegijn.konfetti.models.Size
fun KonfettiView.play() {
val confettiColors = listOf(
R.color.palette_azure,
R.color.palette_grape,
R.color.palette_verde,
R.color.palette_polly,
R.color.palette_melon,
R.color.palette_aqua,
R.color.palette_prune,
R.color.palette_kiwi
)
build()
.addColors(confettiColors.toColorInt(context))
.setDirection(0.0, 359.0)
.setSpeed(2f, 5f)
.setFadeOutEnabled(true)
.setTimeToLive(2000L)
.addShapes(Shape.Square, Shape.Circle)
.addSizes(Size(12))
.setPosition(-50f, width + 50f, -50f, -50f)
.streamFor(150, 3000L)
}
@ColorInt
private fun List<Int>.toColorInt(context: Context) = map { ContextCompat.getColor(context, it) }

View File

@ -103,6 +103,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragm
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthPersonalizationCompleteFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
@ -491,6 +492,11 @@ interface FragmentModule {
@FragmentKey(FtueAuthChooseProfilePictureFragment::class) @FragmentKey(FtueAuthChooseProfilePictureFragment::class)
fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment
@Binds
@IntoMap
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
@Binds @Binds
@IntoMap @IntoMap
@FragmentKey(UserListFragment::class) @FragmentKey(UserListFragment::class)

View File

@ -20,7 +20,6 @@ import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@ -68,6 +67,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vanniktech.emoji.EmojiPopup import com.vanniktech.emoji.EmojiPopup
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.play
import im.vector.app.core.dialogs.ConfirmationDialogBuilder import im.vector.app.core.dialogs.ConfirmationDialogBuilder
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.epoxy.LayoutManagerStateRestorer
@ -204,8 +204,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import nl.dionsegijn.konfetti.models.Shape
import nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
@ -563,16 +561,7 @@ class TimelineFragment @Inject constructor(
when (chatEffect) { when (chatEffect) {
ChatEffect.CONFETTI -> { ChatEffect.CONFETTI -> {
views.viewKonfetti.isVisible = true views.viewKonfetti.isVisible = true
views.viewKonfetti.build() views.viewKonfetti.play()
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA)
.setDirection(0.0, 359.0)
.setSpeed(2f, 5f)
.setFadeOutEnabled(true)
.setTimeToLive(2000L)
.addShapes(Shape.Square, Shape.Circle)
.addSizes(Size(12))
.setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f)
.streamFor(150, 3000L)
} }
ChatEffect.SNOWFALL -> { ChatEffect.SNOWFALL -> {
views.viewSnowFall.isVisible = true views.viewSnowFall.isVisible = true

View File

@ -123,7 +123,7 @@ class LocationPreviewFragment @Inject constructor(
views.mapView.render( views.mapView.render(
MapState( MapState(
zoomOnlyOnce = true, zoomOnlyOnce = true,
pinLocationData = location, userLocationData = location,
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID, pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
pinDrawable = pinDrawable pinDrawable = pinDrawable
) )

View File

@ -19,5 +19,8 @@ package im.vector.app.features.location
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
sealed class LocationSharingAction : VectorViewModelAction { sealed class LocationSharingAction : VectorViewModelAction {
object OnShareLocation : LocationSharingAction() object CurrentUserLocationSharing : LocationSharingAction()
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
object ZoomToUserLocation : LocationSharingAction()
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.location package im.vector.app.features.location
import android.graphics.drawable.Drawable
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -44,7 +45,7 @@ class LocationSharingFragment @Inject constructor(
private val urlMapProvider: UrlMapProvider, private val urlMapProvider: UrlMapProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val matrixItemColorProvider: MatrixItemColorProvider private val matrixItemColorProvider: MatrixItemColorProvider
) : VectorBaseFragment<FragmentLocationSharingBinding>() { ) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTargetChangeListener {
private val viewModel: LocationSharingViewModel by fragmentViewModel() private val viewModel: LocationSharingViewModel by fragmentViewModel()
@ -64,15 +65,20 @@ class LocationSharingFragment @Inject constructor(
views.mapView.onCreate(savedInstanceState) views.mapView.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated { lifecycleScope.launchWhenCreated {
views.mapView.initialize(urlMapProvider.getMapUrl()) views.mapView.initialize(
url = urlMapProvider.getMapUrl(),
locationTargetChangeListener = this@LocationSharingFragment
)
} }
initLocateButton()
initOptionsPicker() initOptionsPicker()
viewModel.observeViewEvents { viewModel.observeViewEvents {
when (it) { when (it) {
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError() LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
LocationSharingViewEvents.Close -> activity?.finish() LocationSharingViewEvents.Close -> activity?.finish()
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
}.exhaustive }.exhaustive
} }
} }
@ -113,10 +119,17 @@ class LocationSharingFragment @Inject constructor(
super.onDestroy() super.onDestroy()
} }
override fun onLocationTargetChange(target: LocationData) {
viewModel.handle(LocationSharingAction.LocationTargetChange(target))
}
override fun invalidate() = withState(viewModel) { state -> override fun invalidate() = withState(viewModel) { state ->
views.mapView.render(state.toMapState()) updateMap(state)
views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
updateUserAvatar(state.userItem) updateUserAvatar(state.userItem)
if (state.locationTargetDrawable != null) {
updateLocationTargetPin(state.locationTargetDrawable)
}
views.shareLocationGpsLoading.isGone = state.lastKnownUserLocation != null
} }
private fun handleLocationNotAvailableError() { private fun handleLocationNotAvailableError() {
@ -130,21 +143,52 @@ class LocationSharingFragment @Inject constructor(
.show() .show()
} }
private fun initLocateButton() {
views.mapView.locateButton.setOnClickListener {
viewModel.handle(LocationSharingAction.ZoomToUserLocation)
}
}
private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) {
views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
}
private fun initOptionsPicker() { private fun initOptionsPicker() {
// TODO // set no option at start
// change the options dynamically depending on the current chosen location views.shareLocationOptionsPicker.render()
views.shareLocationOptionsPicker.render(LocationSharingOption.USER_CURRENT)
views.shareLocationOptionsPicker.optionPinned.debouncedClicks { views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
// TODO val targetLocation = views.mapView.getLocationOfMapCenter()
viewModel.handle(LocationSharingAction.PinnedLocationSharing(targetLocation))
} }
views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks { views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
viewModel.handle(LocationSharingAction.OnShareLocation) viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
} }
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks { views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
// TODO // TODO
} }
} }
private fun updateMap(state: LocationSharingViewState) {
// first, update the options view
when (state.areTargetAndUserLocationEqual) {
// TODO activate USER_LIVE option when implemented
true -> views.shareLocationOptionsPicker.render(
LocationSharingOption.USER_CURRENT
)
false -> views.shareLocationOptionsPicker.render(
LocationSharingOption.PINNED
)
else -> views.shareLocationOptionsPicker.render()
}
// then, update the map using the height of the options view after it has been rendered
views.shareLocationOptionsPicker.post {
val mapState = state
.toMapState()
.copy(logoMarginBottom = views.shareLocationOptionsPicker.height)
views.mapView.render(mapState)
}
}
private fun updateUserAvatar(userItem: MatrixItem.UserItem?) { private fun updateUserAvatar(userItem: MatrixItem.UserItem?) {
userItem?.takeUnless { hasRenderedUserAvatar } userItem?.takeUnless { hasRenderedUserAvatar }
?.let { ?.let {
@ -154,4 +198,8 @@ class LocationSharingFragment @Inject constructor(
views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor) views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor)
} }
} }
private fun updateLocationTargetPin(drawable: Drawable) {
views.shareLocationPin.setImageDrawable(drawable)
}
} }

View File

@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class LocationSharingViewEvents : VectorViewEvents { sealed class LocationSharingViewEvents : VectorViewEvents {
object Close : LocationSharingViewEvents() object Close : LocationSharingViewEvents()
object LocationNotAvailableError : LocationSharingViewEvents() object LocationNotAvailableError : LocationSharingViewEvents()
data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents()
} }

View File

@ -16,6 +16,7 @@
package im.vector.app.features.location package im.vector.app.features.location
import android.graphics.drawable.Drawable
import com.airbnb.mvrx.MavericksViewModelFactory import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory import dagger.assisted.AssistedFactory
@ -25,18 +26,36 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.extensions.exhaustive import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel import im.vector.app.core.platform.VectorViewModel
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.domain.usecase.CompareLocationsUseCase
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.lastOrNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.toMatrixItem import org.matrix.android.sdk.api.util.toMatrixItem
/**
* Sampling period to compare target location and user location.
*/
private const val TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS = 100L
class LocationSharingViewModel @AssistedInject constructor( class LocationSharingViewModel @AssistedInject constructor(
@Assisted private val initialState: LocationSharingViewState, @Assisted private val initialState: LocationSharingViewState,
private val locationTracker: LocationTracker, private val locationTracker: LocationTracker,
private val locationPinProvider: LocationPinProvider, private val locationPinProvider: LocationPinProvider,
private val session: Session private val session: Session,
private val compareLocationsUseCase: CompareLocationsUseCase
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback { ) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
private val room = session.getRoom(initialState.roomId)!! private val room = session.getRoom(initialState.roomId)!!
private val locationTargetFlow = MutableSharedFlow<LocationData>()
@AssistedFactory @AssistedFactory
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> { interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
@ -47,21 +66,47 @@ class LocationSharingViewModel @AssistedInject constructor(
init { init {
locationTracker.start(this) locationTracker.start(this)
setUserItem() setUserItem()
createPin() updatePin()
compareTargetAndUserLocation()
} }
private fun setUserItem() { private fun setUserItem() {
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) } setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
} }
private fun createPin() { private fun updatePin(isUserPin: Boolean? = true) {
locationPinProvider.create(session.myUserId) { if (isUserPin.orFalse()) {
locationPinProvider.create(userId = session.myUserId) {
updatePinDrawableInState(it)
}
} else {
locationPinProvider.create(userId = null) {
updatePinDrawableInState(it)
}
}
}
private fun updatePinDrawableInState(drawable: Drawable) {
setState { setState {
copy( copy(
pinDrawable = it locationTargetDrawable = drawable
) )
} }
} }
private fun compareTargetAndUserLocation() {
locationTargetFlow
.sample(TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS)
.map { compareTargetLocation(it) }
.distinctUntilChanged()
.onEach { setState { copy(areTargetAndUserLocationEqual = it) } }
.onEach { updatePin(isUserPin = it) }
.launchIn(viewModelScope)
}
private suspend fun compareTargetLocation(targetLocation: LocationData): Boolean? {
return awaitState().lastKnownUserLocation
?.let { userLocation -> compareLocationsUseCase.execute(userLocation, targetLocation) }
} }
override fun onCleared() { override fun onCleared() {
@ -71,16 +116,28 @@ class LocationSharingViewModel @AssistedInject constructor(
override fun handle(action: LocationSharingAction) { override fun handle(action: LocationSharingAction) {
when (action) { when (action) {
LocationSharingAction.OnShareLocation -> handleShareLocation() LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
}.exhaustive }.exhaustive
} }
private fun handleShareLocation() = withState { state -> private fun handleCurrentUserLocationSharingAction() = withState { state ->
state.lastKnownLocation?.let { location -> shareLocation(state.lastKnownUserLocation, isUserLocation = true)
}
private fun handlePinnedLocationSharingAction(action: LocationSharingAction.PinnedLocationSharing) {
shareLocation(action.locationData, isUserLocation = false)
}
private fun shareLocation(locationData: LocationData?, isUserLocation: Boolean) {
locationData?.let { location ->
room.sendLocation( room.sendLocation(
latitude = location.latitude, latitude = location.latitude,
longitude = location.longitude, longitude = location.longitude,
uncertainty = location.uncertainty uncertainty = location.uncertainty,
isUserLocation = isUserLocation
) )
_viewEvents.post(LocationSharingViewEvents.Close) _viewEvents.post(LocationSharingViewEvents.Close)
} ?: run { } ?: run {
@ -88,9 +145,27 @@ class LocationSharingViewModel @AssistedInject constructor(
} }
} }
private fun handleLocationTargetChangeAction(action: LocationSharingAction.LocationTargetChange) {
viewModelScope.launch {
locationTargetFlow.emit(action.locationData)
}
}
private fun handleZoomToUserLocationAction() = withState { state ->
state.lastKnownUserLocation?.let { location ->
_viewEvents.post(LocationSharingViewEvents.ZoomToUserLocation(location))
}
}
override fun onLocationUpdate(locationData: LocationData) { override fun onLocationUpdate(locationData: LocationData) {
setState { setState {
copy(lastKnownLocation = locationData) copy(lastKnownUserLocation = locationData)
}
viewModelScope.launch {
// recompute location comparison using last received target location
locationTargetFlow.lastOrNull()?.let {
locationTargetFlow.emit(it)
}
} }
} }

View File

@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.airbnb.mvrx.MavericksState import com.airbnb.mvrx.MavericksState
import im.vector.app.R import im.vector.app.R
import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
enum class LocationSharingMode(@StringRes val titleRes: Int) { enum class LocationSharingMode(@StringRes val titleRes: Int) {
@ -31,8 +32,9 @@ data class LocationSharingViewState(
val roomId: String, val roomId: String,
val mode: LocationSharingMode, val mode: LocationSharingMode,
val userItem: MatrixItem.UserItem? = null, val userItem: MatrixItem.UserItem? = null,
val lastKnownLocation: LocationData? = null, val areTargetAndUserLocationEqual: Boolean? = null,
val pinDrawable: Drawable? = null val lastKnownUserLocation: LocationData? = null,
val locationTargetDrawable: Drawable? = null
) : MavericksState { ) : MavericksState {
constructor(locationSharingArgs: LocationSharingArgs) : this( constructor(locationSharingArgs: LocationSharingArgs) : this(
@ -43,7 +45,9 @@ data class LocationSharingViewState(
fun LocationSharingViewState.toMapState() = MapState( fun LocationSharingViewState.toMapState() = MapState(
zoomOnlyOnce = true, zoomOnlyOnce = true,
pinLocationData = lastKnownLocation, userLocationData = lastKnownUserLocation,
pinId = DEFAULT_PIN_ID, pinId = DEFAULT_PIN_ID,
pinDrawable = pinDrawable pinDrawable = null,
// show the map pin only when target location and user location are not equal
showPin = areTargetAndUserLocationEqual.orTrue().not()
) )

View File

@ -0,0 +1,21 @@
/*
* 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
interface LocationTargetChangeListener {
fun onLocationTargetChange(target: LocationData)
}

View File

@ -17,10 +17,13 @@
package im.vector.app.features.location package im.vector.app.features.location
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.Px
data class MapState( data class MapState(
val zoomOnlyOnce: Boolean, val zoomOnlyOnce: Boolean,
val pinLocationData: LocationData? = null, val userLocationData: LocationData? = null,
val pinId: String, val pinId: String,
val pinDrawable: Drawable? = null val pinDrawable: Drawable? = null,
val showPin: Boolean = true,
@Px val logoMarginBottom: Int = 0
) )

View File

@ -17,7 +17,14 @@
package im.vector.app.features.location package im.vector.app.features.location
import android.content.Context import android.content.Context
import android.content.res.TypedArray
import android.util.AttributeSet import android.util.AttributeSet
import android.view.Gravity
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.view.marginBottom
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import com.mapbox.mapboxsdk.camera.CameraPosition import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.geometry.LatLng import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.maps.MapView import com.mapbox.mapboxsdk.maps.MapView
@ -26,6 +33,7 @@ import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager 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 im.vector.app.R
import timber.log.Timber import timber.log.Timber
class MapTilerMapView @JvmOverloads constructor( class MapTilerMapView @JvmOverloads constructor(
@ -42,15 +50,50 @@ class MapTilerMapView @JvmOverloads constructor(
val style: Style val style: Style
) )
private val userLocationDrawable by lazy {
ContextCompat.getDrawable(context, R.drawable.ic_location_user)
}
val locateButton by lazy { createLocateButton() }
private var mapRefs: MapRefs? = null private var mapRefs: MapRefs? = null
private var initZoomDone = false private var initZoomDone = false
private var showLocationButton = false
init {
context.theme.obtainStyledAttributes(
attrs,
R.styleable.MapTilerMapView,
0,
0
).run {
try {
setLocateButtonVisibility(this)
} finally {
recycle()
}
}
}
private fun setLocateButtonVisibility(typedArray: TypedArray) {
showLocationButton = typedArray.getBoolean(R.styleable.MapTilerMapView_showLocateButton, false)
}
/** /**
* For location fragments * For location fragments
*/ */
fun initialize(url: String) { fun initialize(
url: String,
locationTargetChangeListener: LocationTargetChangeListener? = null
) {
Timber.d("## Location: initialize") Timber.d("## Location: initialize")
getMapAsync { map -> getMapAsync { map ->
initMapStyle(map, url)
initLocateButton(map)
notifyLocationOfMapCenter(locationTargetChangeListener)
listenCameraMove(map, locationTargetChangeListener)
}
}
private fun initMapStyle(map: MapboxMap, url: String) {
map.setStyle(url) { style -> map.setStyle(url) { style ->
mapRefs = MapRefs( mapRefs = MapRefs(
map, map,
@ -61,6 +104,47 @@ class MapTilerMapView @JvmOverloads constructor(
pendingState = null pendingState = null
} }
} }
private fun initLocateButton(map: MapboxMap) {
if (showLocationButton) {
addView(locateButton)
adjustCompassButton(map)
}
}
private fun createLocateButton(): ImageView =
ImageView(context).apply {
setImageDrawable(ContextCompat.getDrawable(context, R.drawable.btn_locate))
contentDescription = context.getString(R.string.a11y_location_share_locate_button)
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
updateLayoutParams<MarginLayoutParams> {
val marginHorizontal = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_locate_button_margin_horizontal)
val marginVertical = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_locate_button_margin_vertical)
setMargins(marginHorizontal, marginVertical, marginHorizontal, marginVertical)
}
updateLayoutParams<LayoutParams> {
gravity = Gravity.TOP or Gravity.END
}
}
private fun adjustCompassButton(map: MapboxMap) {
locateButton.post {
val marginTop = locateButton.height + locateButton.marginTop + locateButton.marginBottom
val marginRight = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_compass_button_margin_horizontal)
map.uiSettings.setCompassMargins(0, marginTop, marginRight, 0)
}
}
private fun listenCameraMove(map: MapboxMap, locationTargetChangeListener: LocationTargetChangeListener?) {
map.addOnCameraMoveListener {
notifyLocationOfMapCenter(locationTargetChangeListener)
}
}
private fun notifyLocationOfMapCenter(locationTargetChangeListener: LocationTargetChangeListener?) {
getLocationOfMapCenter()?.let { target ->
locationTargetChangeListener?.onLocationTargetChange(target)
}
} }
fun render(state: MapState) { fun render(state: MapState) {
@ -68,20 +152,24 @@ class MapTilerMapView @JvmOverloads constructor(
pendingState = state pendingState = state
} }
state.pinDrawable?.let { pinDrawable -> safeMapRefs.map.uiSettings.setLogoMargins(0, 0, 0, state.logoMarginBottom)
val pinDrawable = state.pinDrawable ?: userLocationDrawable
pinDrawable?.let { drawable ->
if (!safeMapRefs.style.isFullyLoaded || if (!safeMapRefs.style.isFullyLoaded ||
safeMapRefs.style.getImage(state.pinId) == null) { safeMapRefs.style.getImage(state.pinId) == null) {
safeMapRefs.style.addImage(state.pinId, pinDrawable) safeMapRefs.style.addImage(state.pinId, drawable)
} }
} }
state.pinLocationData?.let { locationData -> state.userLocationData?.let { locationData ->
if (!initZoomDone || !state.zoomOnlyOnce) { if (!initZoomDone || !state.zoomOnlyOnce) {
zoomToLocation(locationData.latitude, locationData.longitude) zoomToLocation(locationData.latitude, locationData.longitude)
initZoomDone = true initZoomDone = true
} }
safeMapRefs.symbolManager.deleteAll() safeMapRefs.symbolManager.deleteAll()
if (pinDrawable != null && state.showPin) {
safeMapRefs.symbolManager.create( safeMapRefs.symbolManager.create(
SymbolOptions() SymbolOptions()
.withLatLng(LatLng(locationData.latitude, locationData.longitude)) .withLatLng(LatLng(locationData.latitude, locationData.longitude))
@ -90,12 +178,22 @@ class MapTilerMapView @JvmOverloads constructor(
) )
} }
} }
}
private fun zoomToLocation(latitude: Double, longitude: Double) { fun zoomToLocation(latitude: Double, longitude: Double) {
Timber.d("## Location: zoomToLocation") Timber.d("## Location: zoomToLocation")
mapRefs?.map?.cameraPosition = CameraPosition.Builder() mapRefs?.map?.cameraPosition = CameraPosition.Builder()
.target(LatLng(latitude, longitude)) .target(LatLng(latitude, longitude))
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW) .zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
.build() .build()
} }
fun getLocationOfMapCenter(): LocationData? =
mapRefs?.map?.cameraPosition?.target?.let { target ->
LocationData(
latitude = target.latitude,
longitude = target.longitude,
uncertainty = null
)
}
} }

View File

@ -0,0 +1,48 @@
/*
* 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.domain.usecase
import com.mapbox.mapboxsdk.geometry.LatLng
import im.vector.app.features.location.LocationData
import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.session.Session
import javax.inject.Inject
/**
* Threshold in meters to consider 2 locations as equal.
*/
private const val SAME_LOCATION_THRESHOLD_IN_METERS = 5
/**
* Use case to check if 2 locations can be considered as equal.
*/
class CompareLocationsUseCase @Inject constructor(
private val session: Session
) {
/**
* Compare the 2 given locations.
* @return true when they are really close and could be considered as the same location, false otherwise
*/
suspend fun execute(location1: LocationData, location2: LocationData): Boolean =
withContext(session.coroutineDispatchers.io) {
val loc1 = LatLng(location1.latitude, location1.longitude)
val loc2 = LatLng(location2.latitude, location2.longitude)
val distance = loc1.distanceTo(loc2)
distance <= SAME_LOCATION_THRESHOLD_IN_METERS
}
}

View File

@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.events.model.EventType import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isEdition import org.matrix.android.sdk.api.session.events.model.isEdition
import org.matrix.android.sdk.api.session.events.model.isImageMessage import org.matrix.android.sdk.api.session.events.model.isImageMessage
import org.matrix.android.sdk.api.session.events.model.supportsNotification
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.Membership import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
@ -94,7 +95,7 @@ class NotifiableEventResolver @Inject constructor(
} }
suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? { suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
if (event.getClearType() != EventType.MESSAGE) return null if (!event.supportsNotification()) return null
// Ignore message edition // Ignore message edition
if (event.isEdition()) return null if (event.isEdition()) return null
@ -153,7 +154,8 @@ class NotifiableEventResolver @Inject constructor(
event.attemptToDecryptIfNeeded(session) event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents // only convert encrypted messages to NotifiableMessageEvents
when (event.root.getClearType()) { when (event.root.getClearType()) {
EventType.MESSAGE -> { EventType.MESSAGE,
in EventType.POLL_START -> {
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString() val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
val roomName = room.roomSummary()?.displayName ?: "" val roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName val senderDisplayName = event.senderInfo.disambiguatedDisplayName

View File

@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.animations.play
import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
import im.vector.app.features.onboarding.OnboardingAction import im.vector.app.features.onboarding.OnboardingAction
@ -33,6 +34,8 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder private val activeSessionHolder: ActiveSessionHolder
) : AbstractFtueAuthFragment<FragmentFtueAccountCreatedBinding>() { ) : AbstractFtueAuthFragment<FragmentFtueAccountCreatedBinding>() {
private var hasPlayedConfetti = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAccountCreatedBinding { override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAccountCreatedBinding {
return FragmentFtueAccountCreatedBinding.inflate(inflater, container, false) return FragmentFtueAccountCreatedBinding.inflate(inflater, container, false)
} }
@ -53,6 +56,12 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
val canPersonalize = state.personalizationState.supportsPersonalization() val canPersonalize = state.personalizationState.supportsPersonalization()
views.personalizeButtonGroup.isVisible = canPersonalize views.personalizeButtonGroup.isVisible = canPersonalize
views.takeMeHomeButtonGroup.isVisible = !canPersonalize views.takeMeHomeButtonGroup.isVisible = !canPersonalize
if (!hasPlayedConfetti && !canPersonalize) {
hasPlayedConfetti = true
views.viewKonfetti.isVisible = true
views.viewKonfetti.play()
}
} }
override fun resetViewModel() { override fun resetViewModel() {

View File

@ -0,0 +1,61 @@
/*
* Copyright (c) 2021 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.onboarding.ftueauth
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import im.vector.app.core.animations.play
import im.vector.app.databinding.FragmentFtuePersonalizationCompleteBinding
import im.vector.app.features.onboarding.OnboardingAction
import im.vector.app.features.onboarding.OnboardingViewEvents
import javax.inject.Inject
class FtueAuthPersonalizationCompleteFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtuePersonalizationCompleteBinding>() {
private var hasPlayedConfetti = false
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtuePersonalizationCompleteBinding {
return FragmentFtuePersonalizationCompleteBinding.inflate(inflater, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupViews()
}
private fun setupViews() {
views.personalizationCompleteCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
if (!hasPlayedConfetti) {
hasPlayedConfetti = true
views.viewKonfetti.isVisible = true
views.viewKonfetti.play()
}
}
override fun resetViewModel() {
// Nothing to do
}
override fun onBackPressed(toolbarButton: Boolean): Boolean {
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
return true
}
}

View File

@ -227,7 +227,7 @@ class FtueAuthVariant(
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName() OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true) OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture() OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true) OnboardingViewEvents.OnPersonalizationComplete -> onPersonalizationComplete()
OnboardingViewEvents.OnBack -> activity.popBackstack() OnboardingViewEvents.OnBack -> activity.popBackstack()
}.exhaustive }.exhaustive
} }
@ -393,7 +393,8 @@ class FtueAuthVariant(
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE) activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.replaceFragment( activity.replaceFragment(
views.loginFragmentContainer, views.loginFragmentContainer,
FtueAuthAccountCreatedFragment::class.java FtueAuthAccountCreatedFragment::class.java,
useCustomAnimation = true
) )
} }
@ -416,4 +417,13 @@ class FtueAuthVariant(
option = commonOption option = commonOption
) )
} }
private fun onPersonalizationComplete() {
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.replaceFragment(
views.loginFragmentContainer,
FtueAuthPersonalizationCompleteFragment::class.java,
useCustomAnimation = true
)
}
} }

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/palette_white" />
<corners android:radius="4dp" />
</shape>
</item>
<item android:drawable="?selectableItemBackground" />
<item
android:bottom="8dp"
android:drawable="@drawable/ic_locate"
android:left="8dp"
android:right="8dp"
android:top="8dp" />
</layer-list>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="70"
android:viewportHeight="70">
<path
android:pathData="M21,23L19,27L23,25L27,27L25,23L27,19L23,21L19,19L21,23Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#FF0000"
android:strokeColor="#FF0000"/>
<path
android:pathData="M35.653,41.423L38.538,50.076L41.422,41.423L50.076,38.538L41.422,35.654L38.538,27L35.653,35.654L27,38.538L35.653,41.423Z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#FF0000"
android:strokeColor="#FF0000"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="22dp"
android:viewportWidth="22"
android:viewportHeight="22">
<path
android:pathData="M11,7C8.79,7 7,8.79 7,11C7,13.21 8.79,15 11,15C13.21,15 15,13.21 15,11C15,8.79 13.21,7 11,7ZM19.94,10C19.48,5.83 16.17,2.52 12,2.06V1C12,0.45 11.55,0 11,0C10.45,0 10,0.45 10,1V2.06C5.83,2.52 2.52,5.83 2.06,10H1C0.45,10 0,10.45 0,11C0,11.55 0.45,12 1,12H2.06C2.52,16.17 5.83,19.48 10,19.94V21C10,21.55 10.45,22 11,22C11.55,22 12,21.55 12,21V19.94C16.17,19.48 19.48,16.17 19.94,12H21C21.55,12 22,11.55 22,11C22,10.45 21.55,10 21,10H19.94ZM11,18C7.13,18 4,14.87 4,11C4,7.13 7.13,4 11,4C14.87,4 18,7.13 18,11C18,14.87 14.87,18 11,18Z"
android:fillColor="#0DBD8B"/>
</vector>

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="13dp"
android:height="13dp" />
<solid android:color="?colorPrimary" />
<stroke
android:width="2dp"
android:color="@color/palette_white" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="70dp"
android:viewportWidth="70"
android:viewportHeight="70">
<path
android:pathData="M35,36.742C40.771,36.742 45.45,31.673 45.45,25.421C45.45,19.169 40.771,14.1 35,14.1C29.229,14.1 24.55,19.169 24.55,25.421C24.55,31.673 29.229,36.742 35,36.742ZM35,62.867C42.531,62.867 49.364,59.879 54.379,55.025C51.278,47.368 43.77,41.967 35,41.967C26.23,41.967 18.722,47.368 15.621,55.025C20.636,59.879 27.469,62.867 35,62.867Z"
android:fillColor="#FF0000"
android:fillType="evenOdd"/>
</vector>

View File

@ -6,6 +6,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?colorSecondary"> android:background="?colorSecondary">
<nl.dionsegijn.konfetti.KonfettiView
android:id="@+id/viewKonfetti"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/ftueAuthGutterStart" android:id="@+id/ftueAuthGutterStart"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -34,14 +40,16 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="@color/element_background_light"
android:importantForAccessibility="no" android:importantForAccessibility="no"
android:src="@drawable/ic_user_round" android:src="@drawable/ic_user_fg"
app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace2" app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace2"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.15" app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace1" app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace1"
app:tint="@color/element_background_light" /> app:tint="?colorSecondary" />
<Space <Space
android:id="@+id/accountCreatedSpace2" android:id="@+id/accountCreatedSpace2"

View File

@ -39,13 +39,15 @@
android:layout_height="0dp" android:layout_height="0dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:contentDescription="@null" android:contentDescription="@null"
android:src="@drawable/ic_user_round" android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:src="@drawable/ic_user_fg"
app:layout_constraintBottom_toTopOf="@id/displayNameHeaderTitle" app:layout_constraintBottom_toTopOf="@id/displayNameHeaderTitle"
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd" app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
app:layout_constraintHeight_percent="0.15" app:layout_constraintHeight_percent="0.12"
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart" app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:tint="?colorSecondary" /> app:tint="@color/palette_white" />
<TextView <TextView
android:id="@+id/displayNameHeaderTitle" android:id="@+id/displayNameHeaderTitle"

View File

@ -0,0 +1,115 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<nl.dionsegijn.konfetti.KonfettiView
android:id="@+id/viewKonfetti"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/ftueAuthGutterStart"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/ftueAuthGutterEnd"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
<Space
android:id="@+id/personalizationCompleteSpace1"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteLogo"
app:layout_constraintHeight_percent="0.10"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="spread_inside" />
<ImageView
android:id="@+id/personalizationCompleteLogo"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
android:background="@drawable/circle"
android:backgroundTint="?colorSecondary"
android:importantForAccessibility="no"
android:src="@drawable/ic_celebration"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace2"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent="0.15"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace1"
app:tint="@color/palette_white" />
<Space
android:id="@+id/personalizationCompleteSpace2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteTitle"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteLogo" />
<TextView
android:id="@+id/personalizationCompleteTitle"
style="@style/Widget.Vector.TextView.Title.Medium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/ftue_personalize_complete_title"
android:textColor="?vctr_content_primary"
android:transitionName="loginTitleTransition"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSubtitle"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace2" />
<TextView
android:id="@+id/personalizationCompleteSubtitle"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:text="@string/ftue_personalize_complete_subtitle"
android:textColor="?vctr_content_secondary"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace4"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteTitle" />
<Space
android:id="@+id/personalizationCompleteSpace4"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteCta"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSubtitle" />
<Button
android:id="@+id/personalizationCompleteCta"
style="@style/Widget.Vector.Button.Login"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/ftue_personalize_lets_go"
android:textAllCaps="true"
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace5"
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace4" />
<Space
android:id="@+id/personalizationCompleteSpace5"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHeight_percent="0.05"
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteCta" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -31,6 +31,7 @@
style="@style/Widget.Vector.Toolbar.Settings" style="@style/Widget.Vector.Toolbar.Settings"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:visibility="invisible" android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/profilePictureView" app:layout_constraintBottom_toTopOf="@id/profilePictureView"
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar" app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
@ -44,9 +45,9 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:background="@drawable/bg_rounded_button"
android:contentDescription="@null" android:contentDescription="@null"
android:foreground="@drawable/bg_rounded_button" android:src="@drawable/ic_user_fg"
android:src="@drawable/ic_user_round"
app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing" app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing"
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd" app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height" app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height"

View File

@ -8,6 +8,7 @@
android:id="@+id/mapView" android:id="@+id/mapView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:mapbox_renderTextureMode="true" /> app:mapbox_renderTextureMode="true"
app:showLocateButton="false" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -7,13 +7,34 @@
<im.vector.app.features.location.MapTilerMapView <im.vector.app.features.location.MapTilerMapView
android:id="@+id/mapView" android:id="@+id/mapView"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/shareLocationOptionsPicker" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:mapbox_renderTextureMode="true" app:mapbox_renderTextureMode="true"
app:showLocateButton="true"
tools:background="#4F00" /> tools:background="#4F00" />
<ImageView
android:id="@+id/shareLocationPin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/a11y_location_share_pin_on_map"
app:layout_constraintBottom_toTopOf="@id/shareLocationMapCenter"
app:layout_constraintEnd_toEndOf="@id/mapView"
app:layout_constraintStart_toStartOf="@id/mapView" />
<ViewStub
android:id="@+id/shareLocationMapCenter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="@id/mapView"
app:layout_constraintEnd_toEndOf="@id/mapView"
app:layout_constraintStart_toStartOf="@id/mapView"
app:layout_constraintTop_toTopOf="@id/mapView" />
<im.vector.app.features.location.option.LocationSharingOptionPickerView <im.vector.app.features.location.option.LocationSharingOptionPickerView
android:id="@+id/shareLocationOptionsPicker" android:id="@+id/shareLocationOptionsPicker"
android:layout_width="0dp" android:layout_width="0dp"

View File

@ -22,7 +22,9 @@
<string name="ftue_profile_picture_title" translatable="false">Add a profile picture</string> <string name="ftue_profile_picture_title" translatable="false">Add a profile picture</string>
<string name="ftue_profile_picture_subtitle" translatable="false">You can change this anytime.</string> <string name="ftue_profile_picture_subtitle" translatable="false">You can change this anytime.</string>
<string name="ftue_personalize_lets_go" translatable="false">Let\'s go</string>
<string name="ftue_personalize_complete_title" translatable="false">You\'re all set!</string>
<string name="ftue_personalize_complete_subtitle" translatable="false">Your preferences have been saved.</string>
<string name="ftue_personalize_submit" translatable="false">Save and continue</string> <string name="ftue_personalize_submit" translatable="false">Save and continue</string>
<string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string> <string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string>

View File

@ -2929,6 +2929,8 @@
<string name="a11y_static_map_image">Map</string> <string name="a11y_static_map_image">Map</string>
<!-- TODO delete --> <!-- TODO delete -->
<string name="location_share" tools:ignore="UnusedResources">Share location</string> <string name="location_share" tools:ignore="UnusedResources">Share location</string>
<string name="a11y_location_share_pin_on_map">Pin of selected location on map</string>
<string name="a11y_location_share_locate_button">Zoom to current location</string>
<string name="location_share_option_user_current">Share my current location</string> <string name="location_share_option_user_current">Share my current location</string>
<string name="a11y_location_share_option_user_current_icon">Share my current location</string> <string name="a11y_location_share_option_user_current_icon">Share my current location</string>
<string name="location_share_option_user_live">Share live location</string> <string name="location_share_option_user_live">Share live location</string>

View File

@ -0,0 +1,83 @@
/*
* 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.domain.usecase
import com.airbnb.mvrx.test.MvRxTestRule
import im.vector.app.features.location.LocationData
import im.vector.app.test.fakes.FakeSession
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.OverrideMockKs
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
class CompareLocationsUseCaseTest {
@get:Rule
val mvRxTestRule = MvRxTestRule()
private val session = FakeSession()
@OverrideMockKs
lateinit var compareLocationsUseCase: CompareLocationsUseCase
@Before
fun setUp() {
MockKAnnotations.init(this)
}
@Test
fun `given 2 very near locations when calling execute then these locations are considered as equal`() = runBlockingTest {
// Given
val location1 = LocationData(
latitude = 48.858269,
longitude = 2.294551,
uncertainty = null
)
val location2 = LocationData(
latitude = 48.858275,
longitude = 2.294547,
uncertainty = null
)
// When
val areEqual = compareLocationsUseCase.execute(location1, location2)
// Then
assert(areEqual)
}
@Test
fun `given 2 far away locations when calling execute then these locations are considered as not equal`() = runBlockingTest {
// Given
val location1 = LocationData(
latitude = 48.858269,
longitude = 2.294551,
uncertainty = null
)
val location2 = LocationData(
latitude = 48.861777,
longitude = 2.289348,
uncertainty = null
)
// When
val areEqual = compareLocationsUseCase.execute(location1, location2)
// Then
assert(areEqual.not())
}
}