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" /> + + + + + + + + + + + + + + + + + + + + +