diff --git a/changelog.d/4780.bugfix b/changelog.d/4780.bugfix
new file mode 100644
index 0000000000..51eb1e4ad7
--- /dev/null
+++ b/changelog.d/4780.bugfix
@@ -0,0 +1 @@
+Poll system notifications on Android are not user friendly
\ No newline at end of file
diff --git a/changelog.d/5389.wip b/changelog.d/5389.wip
new file mode 100644
index 0000000000..089fe2da1a
--- /dev/null
+++ b/changelog.d/5389.wip
@@ -0,0 +1 @@
+Introduces FTUE personalisation complete screen along with confetti celebration
\ No newline at end of file
diff --git a/changelog.d/5417.feature b/changelog.d/5417.feature
new file mode 100644
index 0000000000..8b64f9fc7f
--- /dev/null
+++ b/changelog.d/5417.feature
@@ -0,0 +1 @@
+Add ability to pin a location on map for sharing
diff --git a/changelog.d/5521.bugfix b/changelog.d/5521.bugfix
new file mode 100644
index 0000000000..851396a770
--- /dev/null
+++ b/changelog.d/5521.bugfix
@@ -0,0 +1 @@
+Fix mentions using matrix.to rather than client defined permalink base url
diff --git a/library/ui-styles/src/main/res/values/dimens.xml b/library/ui-styles/src/main/res/values/dimens.xml
index 6737f4faf1..600c73c878 100644
--- a/library/ui-styles/src/main/res/values/dimens.xml
+++ b/library/ui-styles/src/main/res/values/dimens.xml
@@ -64,4 +64,7 @@
10dp
+ 16dp
+ 12dp
+ 8dp
diff --git a/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml b/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml
new file mode 100644
index 0000000000..a7c45918af
--- /dev/null
+++ b/library/ui-styles/src/main/res/values/stylable_map_tiler_map_view.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
index 9856ee7770..1e3512a9df 100644
--- a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/MarkdownParserTest.kt
@@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
applicationFlavor = "TestFlavor",
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
)
- ))
+ ),
+ TestPermalinkService()
+ )
)
@Test
diff --git a/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
new file mode 100644
index 0000000000..2f9a5e0a73
--- /dev/null
+++ b/matrix-sdk-android/src/androidTest/java/org/matrix/android/sdk/internal/session/room/send/TestPermalinkService.kt
@@ -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?, 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 -> "%2\$s"
+ MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
+ }
+ }
+}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
index 2ef2dfd91e..f1304f6216 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/events/model/Event.kt
@@ -410,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
fun Event.getPollContent(): MessagePollContent? {
return content.toModel()
}
+
+fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
index 920dc85c7a..c139da813a 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/permalinks/PermalinkService.kt
@@ -28,6 +28,11 @@ interface PermalinkService {
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
}
+ enum class SpanTemplateType {
+ HTML,
+ MARKDOWN
+ }
+
/**
* Creates a permalink for an event.
* 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
*/
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: "%2\$s" 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
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
index e8b3cf2488..35fa555a5b 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAsset.kt
@@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class LocationAsset(
- @Json(name = "type") val type: LocationAssetType? = null
+ @Json(name = "type") val type: String? = null
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
index ef40e21c47..f7d82d4b40 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/LocationAssetType.kt
@@ -16,11 +16,20 @@
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 {
- @Json(name = "m.self")
- SELF
+ /**
+ * Used for pin drop location sharing.
+ **/
+ const val PIN = "m.pin"
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
index 84bf5cf7b7..2052133b06 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/model/message/MessageLocationContent.kt
@@ -42,7 +42,7 @@ data class MessageLocationContent(
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = 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 = "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 = "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.
* 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.
+ * 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 = "m.asset") val locationAsset: LocationAsset? = null
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
index 913dbfd010..9f8b1d93d7 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/room/send/SendService.kt
@@ -142,8 +142,9 @@ interface SendService {
* @param latitude required latitude of the location
* @param longitude required longitude of the location
* @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
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
index 63f41ebf2c..81d5ac835f 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventEntityQueries.kt
@@ -97,6 +97,7 @@ internal fun RealmQuery.filterEvents(filters: TimelineEvent
if (filters.filterEdits) {
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
+ not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
}
if (filters.filterRedacted) {
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
index 10a0d1dcec..a7317506a0 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/database/query/TimelineEventFilter.kt
@@ -26,6 +26,7 @@ internal object TimelineEventFilter {
internal object Content {
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 REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
}
/**
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
index 144ebb5404..196a8c122d 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/DefaultPermalinkService.kt
@@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
override fun getLinkedId(url: String): String? {
return permalinkFactory.getLinkedId(url)
}
+
+ override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
+ return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
+ }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
index 39c1ddfdce..0aeb0467de 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/permalinks/PermalinkFactory.kt
@@ -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.permalinks.PermalinkData
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.SpanTemplateType.HTML
+import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
import org.matrix.android.sdk.internal.di.UserId
import javax.inject.Inject
@@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
?.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
*
@@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
private const val ROOM_PATH = "room/"
private const val USER_PATH = "user/"
private const val GROUP_PATH = "group/"
+ private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "%2\$s"
+ private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
+ private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
}
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
index 28c17f38b6..31c7254ed5 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/DefaultSendService.kt
@@ -128,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
.let { sendEvent(it) }
}
- override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
- return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
+ override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
+ return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
.also { createLocalEcho(it) }
.let { sendEvent(it) }
}
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
index bec0ce97dc..0ba95cc1fb 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/LocalEchoEventFactory.kt
@@ -227,13 +227,15 @@ internal class LocalEchoEventFactory @Inject constructor(
fun createLocationEvent(roomId: String,
latitude: Double,
longitude: Double,
- uncertainty: Double?): Event {
+ uncertainty: Double?,
+ isUserLocation: Boolean): Event {
val geoUri = buildGeoUri(latitude, longitude, uncertainty)
+ val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
val content = MessageLocationContent(
geoUri = geoUri,
body = geoUri,
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
- unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF),
+ unstableLocationAsset = LocationAsset(type = assetType),
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
unstableText = geoUri
)
diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
index ccbfbfcded..fa2e0052ab 100644
--- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
+++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/session/room/send/pills/TextPillsUtils.kt
@@ -16,6 +16,7 @@
package org.matrix.android.sdk.internal.session.room.send.pills
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.util.MatrixItem
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
@@ -28,7 +29,8 @@ import javax.inject.Inject
*/
internal class TextPillsUtils @Inject constructor(
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
*/
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
*/
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? {
@@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
i++
}
}
-
- companion object {
- private const val MENTION_SPAN_TO_HTML_TEMPLATE = "%2\$s"
-
- private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
- }
}
diff --git a/vector/src/main/java/im/vector/app/core/animations/Konfetti.kt b/vector/src/main/java/im/vector/app/core/animations/Konfetti.kt
new file mode 100644
index 0000000000..22764ac5bd
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/animations/Konfetti.kt
@@ -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.toColorInt(context: Context) = map { ContextCompat.getColor(context, it) }
diff --git a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
index 2ffdd7ddf3..4dcfbe16f8 100644
--- a/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
+++ b/vector/src/main/java/im/vector/app/core/di/FragmentModule.kt
@@ -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.FtueAuthGenericTextInputFormFragment
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.FtueAuthResetPasswordMailConfirmationFragment
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
@@ -491,6 +492,11 @@ interface FragmentModule {
@FragmentKey(FtueAuthChooseProfilePictureFragment::class)
fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment
+ @Binds
+ @IntoMap
+ @FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
+ fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
+
@Binds
@IntoMap
@FragmentKey(UserListFragment::class)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
index 1c6611853e..ab64f40159 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt
@@ -20,7 +20,6 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
-import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
@@ -68,6 +67,7 @@ import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.vanniktech.emoji.EmojiPopup
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.GalleryOrCameraDialogHelper
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
@@ -204,8 +204,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import nl.dionsegijn.konfetti.models.Shape
-import nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.session.Session
@@ -563,16 +561,7 @@ class TimelineFragment @Inject constructor(
when (chatEffect) {
ChatEffect.CONFETTI -> {
views.viewKonfetti.isVisible = true
- views.viewKonfetti.build()
- .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)
+ views.viewKonfetti.play()
}
ChatEffect.SNOWFALL -> {
views.viewSnowFall.isVisible = true
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
index db837f4823..5d823e53a6 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationPreviewFragment.kt
@@ -123,7 +123,7 @@ class LocationPreviewFragment @Inject constructor(
views.mapView.render(
MapState(
zoomOnlyOnce = true,
- pinLocationData = location,
+ userLocationData = location,
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
pinDrawable = pinDrawable
)
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
index 01319ef6c7..ec47c23ea7 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingAction.kt
@@ -19,5 +19,8 @@ package im.vector.app.features.location
import im.vector.app.core.platform.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()
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
index b1033f2797..e9e96e676c 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingFragment.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.location
+import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@@ -44,7 +45,7 @@ class LocationSharingFragment @Inject constructor(
private val urlMapProvider: UrlMapProvider,
private val avatarRenderer: AvatarRenderer,
private val matrixItemColorProvider: MatrixItemColorProvider
-) : VectorBaseFragment() {
+) : VectorBaseFragment(), LocationTargetChangeListener {
private val viewModel: LocationSharingViewModel by fragmentViewModel()
@@ -64,15 +65,20 @@ class LocationSharingFragment @Inject constructor(
views.mapView.onCreate(savedInstanceState)
lifecycleScope.launchWhenCreated {
- views.mapView.initialize(urlMapProvider.getMapUrl())
+ views.mapView.initialize(
+ url = urlMapProvider.getMapUrl(),
+ locationTargetChangeListener = this@LocationSharingFragment
+ )
}
+ initLocateButton()
initOptionsPicker()
viewModel.observeViewEvents {
when (it) {
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
LocationSharingViewEvents.Close -> activity?.finish()
+ is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
}.exhaustive
}
}
@@ -113,10 +119,17 @@ class LocationSharingFragment @Inject constructor(
super.onDestroy()
}
+ override fun onLocationTargetChange(target: LocationData) {
+ viewModel.handle(LocationSharingAction.LocationTargetChange(target))
+ }
+
override fun invalidate() = withState(viewModel) { state ->
- views.mapView.render(state.toMapState())
- views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
+ updateMap(state)
updateUserAvatar(state.userItem)
+ if (state.locationTargetDrawable != null) {
+ updateLocationTargetPin(state.locationTargetDrawable)
+ }
+ views.shareLocationGpsLoading.isGone = state.lastKnownUserLocation != null
}
private fun handleLocationNotAvailableError() {
@@ -130,21 +143,52 @@ class LocationSharingFragment @Inject constructor(
.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() {
- // TODO
- // change the options dynamically depending on the current chosen location
- views.shareLocationOptionsPicker.render(LocationSharingOption.USER_CURRENT)
+ // set no option at start
+ views.shareLocationOptionsPicker.render()
views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
- // TODO
+ val targetLocation = views.mapView.getLocationOfMapCenter()
+ viewModel.handle(LocationSharingAction.PinnedLocationSharing(targetLocation))
}
views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
- viewModel.handle(LocationSharingAction.OnShareLocation)
+ viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
}
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
// 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?) {
userItem?.takeUnless { hasRenderedUserAvatar }
?.let {
@@ -154,4 +198,8 @@ class LocationSharingFragment @Inject constructor(
views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor)
}
}
+
+ private fun updateLocationTargetPin(drawable: Drawable) {
+ views.shareLocationPin.setImageDrawable(drawable)
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
index 743daaf5e0..8d31db1119 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewEvents.kt
@@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewEvents
sealed class LocationSharingViewEvents : VectorViewEvents {
object Close : LocationSharingViewEvents()
object LocationNotAvailableError : LocationSharingViewEvents()
+ data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents()
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
index 989ec255e5..25bc482412 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewModel.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.location
+import android.graphics.drawable.Drawable
import com.airbnb.mvrx.MavericksViewModelFactory
import dagger.assisted.Assisted
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.platform.VectorViewModel
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.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(
@Assisted private val initialState: LocationSharingViewState,
private val locationTracker: LocationTracker,
private val locationPinProvider: LocationPinProvider,
- private val session: Session
+ private val session: Session,
+ private val compareLocationsUseCase: CompareLocationsUseCase
) : VectorViewModel(initialState), LocationTracker.Callback {
private val room = session.getRoom(initialState.roomId)!!
+ private val locationTargetFlow = MutableSharedFlow()
+
@AssistedFactory
interface Factory : MavericksAssistedViewModelFactory {
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
@@ -47,23 +66,49 @@ class LocationSharingViewModel @AssistedInject constructor(
init {
locationTracker.start(this)
setUserItem()
- createPin()
+ updatePin()
+ compareTargetAndUserLocation()
}
private fun setUserItem() {
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
}
- private fun createPin() {
- locationPinProvider.create(session.myUserId) {
- setState {
- copy(
- pinDrawable = it
- )
+ private fun updatePin(isUserPin: Boolean? = true) {
+ if (isUserPin.orFalse()) {
+ locationPinProvider.create(userId = session.myUserId) {
+ updatePinDrawableInState(it)
+ }
+ } else {
+ locationPinProvider.create(userId = null) {
+ updatePinDrawableInState(it)
}
}
}
+ private fun updatePinDrawableInState(drawable: Drawable) {
+ setState {
+ copy(
+ 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() {
super.onCleared()
locationTracker.stop()
@@ -71,16 +116,28 @@ class LocationSharingViewModel @AssistedInject constructor(
override fun handle(action: LocationSharingAction) {
when (action) {
- LocationSharingAction.OnShareLocation -> handleShareLocation()
+ LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
+ is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
+ is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
+ LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
}.exhaustive
}
- private fun handleShareLocation() = withState { state ->
- state.lastKnownLocation?.let { location ->
+ private fun handleCurrentUserLocationSharingAction() = withState { state ->
+ 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(
latitude = location.latitude,
longitude = location.longitude,
- uncertainty = location.uncertainty
+ uncertainty = location.uncertainty,
+ isUserLocation = isUserLocation
)
_viewEvents.post(LocationSharingViewEvents.Close)
} ?: 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) {
setState {
- copy(lastKnownLocation = locationData)
+ copy(lastKnownUserLocation = locationData)
+ }
+ viewModelScope.launch {
+ // recompute location comparison using last received target location
+ locationTargetFlow.lastOrNull()?.let {
+ locationTargetFlow.emit(it)
+ }
}
}
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
index e63206f515..ee5ba402e2 100644
--- a/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/location/LocationSharingViewState.kt
@@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable
import androidx.annotation.StringRes
import com.airbnb.mvrx.MavericksState
import im.vector.app.R
+import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.util.MatrixItem
enum class LocationSharingMode(@StringRes val titleRes: Int) {
@@ -31,8 +32,9 @@ data class LocationSharingViewState(
val roomId: String,
val mode: LocationSharingMode,
val userItem: MatrixItem.UserItem? = null,
- val lastKnownLocation: LocationData? = null,
- val pinDrawable: Drawable? = null
+ val areTargetAndUserLocationEqual: Boolean? = null,
+ val lastKnownUserLocation: LocationData? = null,
+ val locationTargetDrawable: Drawable? = null
) : MavericksState {
constructor(locationSharingArgs: LocationSharingArgs) : this(
@@ -43,7 +45,9 @@ data class LocationSharingViewState(
fun LocationSharingViewState.toMapState() = MapState(
zoomOnlyOnce = true,
- pinLocationData = lastKnownLocation,
+ userLocationData = lastKnownUserLocation,
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()
)
diff --git a/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt b/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt
new file mode 100644
index 0000000000..07e3afb399
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/LocationTargetChangeListener.kt
@@ -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)
+}
diff --git a/vector/src/main/java/im/vector/app/features/location/MapState.kt b/vector/src/main/java/im/vector/app/features/location/MapState.kt
index d001457e4f..c4325291a8 100644
--- a/vector/src/main/java/im/vector/app/features/location/MapState.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapState.kt
@@ -17,10 +17,13 @@
package im.vector.app.features.location
import android.graphics.drawable.Drawable
+import androidx.annotation.Px
data class MapState(
val zoomOnlyOnce: Boolean,
- val pinLocationData: LocationData? = null,
+ val userLocationData: LocationData? = null,
val pinId: String,
- val pinDrawable: Drawable? = null
+ val pinDrawable: Drawable? = null,
+ val showPin: Boolean = true,
+ @Px val logoMarginBottom: Int = 0
)
diff --git a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
index dd80f701f6..e3206e231d 100644
--- a/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
+++ b/vector/src/main/java/im/vector/app/features/location/MapTilerMapView.kt
@@ -17,7 +17,14 @@
package im.vector.app.features.location
import android.content.Context
+import android.content.res.TypedArray
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.geometry.LatLng
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.SymbolOptions
import com.mapbox.mapboxsdk.style.layers.Property
+import im.vector.app.R
import timber.log.Timber
class MapTilerMapView @JvmOverloads constructor(
@@ -42,24 +50,100 @@ class MapTilerMapView @JvmOverloads constructor(
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 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
*/
- fun initialize(url: String) {
+ fun initialize(
+ url: String,
+ locationTargetChangeListener: LocationTargetChangeListener? = null
+ ) {
Timber.d("## Location: initialize")
getMapAsync { map ->
- map.setStyle(url) { style ->
- mapRefs = MapRefs(
- map,
- SymbolManager(this, map, style),
- style
- )
- pendingState?.let { render(it) }
- pendingState = null
+ initMapStyle(map, url)
+ initLocateButton(map)
+ notifyLocationOfMapCenter(locationTargetChangeListener)
+ listenCameraMove(map, locationTargetChangeListener)
+ }
+ }
+
+ private fun initMapStyle(map: MapboxMap, url: String) {
+ map.setStyle(url) { style ->
+ mapRefs = MapRefs(
+ map,
+ SymbolManager(this, map, style),
+ style
+ )
+ pendingState?.let { render(it) }
+ 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 {
+ 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 {
+ 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)
}
}
@@ -68,34 +152,48 @@ class MapTilerMapView @JvmOverloads constructor(
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 ||
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) {
zoomToLocation(locationData.latitude, locationData.longitude)
initZoomDone = true
}
safeMapRefs.symbolManager.deleteAll()
- safeMapRefs.symbolManager.create(
- SymbolOptions()
- .withLatLng(LatLng(locationData.latitude, locationData.longitude))
- .withIconImage(state.pinId)
- .withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
- )
+ if (pinDrawable != null && state.showPin) {
+ safeMapRefs.symbolManager.create(
+ SymbolOptions()
+ .withLatLng(LatLng(locationData.latitude, locationData.longitude))
+ .withIconImage(state.pinId)
+ .withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
+ )
+ }
}
}
- private fun zoomToLocation(latitude: Double, longitude: Double) {
+ fun zoomToLocation(latitude: Double, longitude: Double) {
Timber.d("## Location: zoomToLocation")
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
.target(LatLng(latitude, longitude))
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
.build()
}
+
+ fun getLocationOfMapCenter(): LocationData? =
+ mapRefs?.map?.cameraPosition?.target?.let { target ->
+ LocationData(
+ latitude = target.latitude,
+ longitude = target.longitude,
+ uncertainty = null
+ )
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt b/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt
new file mode 100644
index 0000000000..91738541be
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCase.kt
@@ -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
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
index 3cdc9e8c76..3c9b985df5 100644
--- a/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
+++ b/vector/src/main/java/im/vector/app/features/notifications/NotifiableEventResolver.kt
@@ -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.isEdition
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.room.model.Membership
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? {
- if (event.getClearType() != EventType.MESSAGE) return null
+ if (!event.supportsNotification()) return null
// Ignore message edition
if (event.isEdition()) return null
@@ -153,7 +154,8 @@ class NotifiableEventResolver @Inject constructor(
event.attemptToDecryptIfNeeded(session)
// only convert encrypted messages to NotifiableMessageEvents
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 roomName = room.roomSummary()?.displayName ?: ""
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
@@ -185,7 +187,7 @@ class NotifiableEventResolver @Inject constructor(
soundName = null
)
}
- else -> null
+ else -> null
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
index ccfb863a5b..49db52da67 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthAccountCreatedFragment.kt
@@ -22,6 +22,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import im.vector.app.R
+import im.vector.app.core.animations.play
import im.vector.app.core.di.ActiveSessionHolder
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
import im.vector.app.features.onboarding.OnboardingAction
@@ -33,6 +34,8 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
private val activeSessionHolder: ActiveSessionHolder
) : AbstractFtueAuthFragment() {
+ private var hasPlayedConfetti = false
+
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAccountCreatedBinding {
return FragmentFtueAccountCreatedBinding.inflate(inflater, container, false)
}
@@ -53,6 +56,12 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
val canPersonalize = state.personalizationState.supportsPersonalization()
views.personalizeButtonGroup.isVisible = canPersonalize
views.takeMeHomeButtonGroup.isVisible = !canPersonalize
+
+ if (!hasPlayedConfetti && !canPersonalize) {
+ hasPlayedConfetti = true
+ views.viewKonfetti.isVisible = true
+ views.viewKonfetti.play()
+ }
}
override fun resetViewModel() {
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt
new file mode 100644
index 0000000000..6b47b9830c
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthPersonalizationCompleteFragment.kt
@@ -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() {
+
+ 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
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
index 2008726ac3..79a974038b 100644
--- a/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
+++ b/vector/src/main/java/im/vector/app/features/onboarding/ftueauth/FtueAuthVariant.kt
@@ -227,7 +227,7 @@ class FtueAuthVariant(
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
- OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true)
+ OnboardingViewEvents.OnPersonalizationComplete -> onPersonalizationComplete()
OnboardingViewEvents.OnBack -> activity.popBackstack()
}.exhaustive
}
@@ -393,7 +393,8 @@ class FtueAuthVariant(
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
activity.replaceFragment(
views.loginFragmentContainer,
- FtueAuthAccountCreatedFragment::class.java
+ FtueAuthAccountCreatedFragment::class.java,
+ useCustomAnimation = true
)
}
@@ -416,4 +417,13 @@ class FtueAuthVariant(
option = commonOption
)
}
+
+ private fun onPersonalizationComplete() {
+ activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
+ activity.replaceFragment(
+ views.loginFragmentContainer,
+ FtueAuthPersonalizationCompleteFragment::class.java,
+ useCustomAnimation = true
+ )
+ }
}
diff --git a/vector/src/main/res/drawable/btn_locate.xml b/vector/src/main/res/drawable/btn_locate.xml
new file mode 100644
index 0000000000..583b3a97ea
--- /dev/null
+++ b/vector/src/main/res/drawable/btn_locate.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_celebration.xml b/vector/src/main/res/drawable/ic_celebration.xml
new file mode 100644
index 0000000000..bfe05cad99
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_celebration.xml
@@ -0,0 +1,18 @@
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_locate.xml b/vector/src/main/res/drawable/ic_locate.xml
new file mode 100644
index 0000000000..784665fcdd
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_locate.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/vector/src/main/res/drawable/ic_location_user.xml b/vector/src/main/res/drawable/ic_location_user.xml
new file mode 100644
index 0000000000..dc6baca65e
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_location_user.xml
@@ -0,0 +1,14 @@
+
+
+ -
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/drawable/ic_user_fg.xml b/vector/src/main/res/drawable/ic_user_fg.xml
new file mode 100644
index 0000000000..a4893861d8
--- /dev/null
+++ b/vector/src/main/res/drawable/ic_user_fg.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_account_created.xml b/vector/src/main/res/layout/fragment_ftue_account_created.xml
index 65bcdf2b63..5b8dddba22 100644
--- a/vector/src/main/res/layout/fragment_ftue_account_created.xml
+++ b/vector/src/main/res/layout/fragment_ftue_account_created.xml
@@ -6,6 +6,12 @@
android:layout_height="match_parent"
android:background="?colorSecondary">
+
+
+ app:tint="?colorSecondary" />
+ app:tint="@color/palette_white" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/vector/src/main/res/layout/fragment_ftue_profile_picture.xml b/vector/src/main/res/layout/fragment_ftue_profile_picture.xml
index 6ebb5cc2f6..afd6ace16c 100644
--- a/vector/src/main/res/layout/fragment_ftue_profile_picture.xml
+++ b/vector/src/main/res/layout/fragment_ftue_profile_picture.xml
@@ -31,6 +31,7 @@
style="@style/Widget.Vector.Toolbar.Settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/profilePictureView"
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
@@ -44,9 +45,9 @@
android:layout_width="wrap_content"
android:layout_height="0dp"
android:adjustViewBounds="true"
+ android:background="@drawable/bg_rounded_button"
android:contentDescription="@null"
- android:foreground="@drawable/bg_rounded_button"
- android:src="@drawable/ic_user_round"
+ android:src="@drawable/ic_user_fg"
app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing"
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height"
diff --git a/vector/src/main/res/layout/fragment_location_preview.xml b/vector/src/main/res/layout/fragment_location_preview.xml
index c2b3bdd739..5499e5b427 100644
--- a/vector/src/main/res/layout/fragment_location_preview.xml
+++ b/vector/src/main/res/layout/fragment_location_preview.xml
@@ -8,6 +8,7 @@
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:mapbox_renderTextureMode="true" />
+ app:mapbox_renderTextureMode="true"
+ app:showLocateButton="false" />
-
\ No newline at end of file
+
diff --git a/vector/src/main/res/layout/fragment_location_sharing.xml b/vector/src/main/res/layout/fragment_location_sharing.xml
index 3d07e4438d..cd15f418ea 100644
--- a/vector/src/main/res/layout/fragment_location_sharing.xml
+++ b/vector/src/main/res/layout/fragment_location_sharing.xml
@@ -7,13 +7,34 @@
+
+
+
+
Add a profile picture
You can change this anytime.
-
+ Let\'s go
+ You\'re all set!
+ Your preferences have been saved.
Save and continue
Skip this step
diff --git a/vector/src/main/res/values/strings.xml b/vector/src/main/res/values/strings.xml
index 3f142cc738..162ab3e119 100644
--- a/vector/src/main/res/values/strings.xml
+++ b/vector/src/main/res/values/strings.xml
@@ -2929,6 +2929,8 @@
Map
Share location
+ Pin of selected location on map
+ Zoom to current location
Share my current location
Share my current location
Share live location
diff --git a/vector/src/test/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCaseTest.kt b/vector/src/test/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCaseTest.kt
new file mode 100644
index 0000000000..015a27b0c8
--- /dev/null
+++ b/vector/src/test/java/im/vector/app/features/location/domain/usecase/CompareLocationsUseCaseTest.kt
@@ -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())
+ }
+}