Merge branch 'develop' into feature/aris/threads_analytics
This commit is contained in:
commit
eee1ec1423
1
changelog.d/4780.bugfix
Normal file
1
changelog.d/4780.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Poll system notifications on Android are not user friendly
|
1
changelog.d/5389.wip
Normal file
1
changelog.d/5389.wip
Normal file
@ -0,0 +1 @@
|
|||||||
|
Introduces FTUE personalisation complete screen along with confetti celebration
|
1
changelog.d/5417.feature
Normal file
1
changelog.d/5417.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add ability to pin a location on map for sharing
|
1
changelog.d/5521.bugfix
Normal file
1
changelog.d/5521.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fix mentions using matrix.to rather than client defined permalink base url
|
@ -64,4 +64,7 @@
|
|||||||
|
|
||||||
<!-- Location sharing -->
|
<!-- Location sharing -->
|
||||||
<dimen name="location_sharing_option_default_padding">10dp</dimen>
|
<dimen name="location_sharing_option_default_padding">10dp</dimen>
|
||||||
|
<dimen name="location_sharing_locate_button_margin_vertical">16dp</dimen>
|
||||||
|
<dimen name="location_sharing_locate_button_margin_horizontal">12dp</dimen>
|
||||||
|
<dimen name="location_sharing_compass_button_margin_horizontal">8dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="MapTilerMapView">
|
||||||
|
<attr name="showLocateButton" format="boolean" />
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
</resources>
|
@ -60,7 +60,9 @@ class MarkdownParserTest : InstrumentedTest {
|
|||||||
applicationFlavor = "TestFlavor",
|
applicationFlavor = "TestFlavor",
|
||||||
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
|
roomDisplayNameFallbackProvider = TestRoomDisplayNameFallbackProvider()
|
||||||
)
|
)
|
||||||
))
|
),
|
||||||
|
TestPermalinkService()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.matrix.android.sdk.internal.session.room.send
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
|
||||||
|
|
||||||
|
class TestPermalinkService : PermalinkService {
|
||||||
|
override fun createPermalink(event: Event, forceMatrixTo: Boolean): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPermalink(id: String, forceMatrixTo: Boolean): String? {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createPermalink(roomId: String, eventId: String, forceMatrixTo: Boolean): String {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createRoomPermalink(roomId: String, viaServers: List<String>?, forceMatrixTo: Boolean): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLinkedId(url: String): String? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
||||||
|
return when (type) {
|
||||||
|
HTML -> "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
|
||||||
|
MARKDOWN -> "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -410,3 +410,5 @@ fun Event.isInvitation(): Boolean = type == EventType.STATE_ROOM_MEMBER &&
|
|||||||
fun Event.getPollContent(): MessagePollContent? {
|
fun Event.getPollContent(): MessagePollContent? {
|
||||||
return content.toModel<MessagePollContent>()
|
return content.toModel<MessagePollContent>()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Event.supportsNotification() = this.getClearType() in EventType.MESSAGE + EventType.POLL_START
|
||||||
|
@ -28,6 +28,11 @@ interface PermalinkService {
|
|||||||
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
const val MATRIX_TO_URL_BASE = "https://matrix.to/#/"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SpanTemplateType {
|
||||||
|
HTML,
|
||||||
|
MARKDOWN
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a permalink for an event.
|
* Creates a permalink for an event.
|
||||||
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
|
* Ex: "https://matrix.to/#/!nbzmcXAqpxBXjAdgoX:matrix.org/$1531497316352799BevdV:matrix.org"
|
||||||
@ -80,4 +85,15 @@ interface PermalinkService {
|
|||||||
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
|
* @return the id from the url, ex: "@benoit:matrix.org", or null if the url is not a permalink
|
||||||
*/
|
*/
|
||||||
fun getLinkedId(url: String): String?
|
fun getLinkedId(url: String): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a HTML or Markdown mention span template. Can be used to replace a mention with a permalink to mentioned user.
|
||||||
|
* Ex: "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>" or "[%2\$s](https://matrix.to/#/%1\$s)"
|
||||||
|
*
|
||||||
|
* @param type: type of template to create
|
||||||
|
* @param forceMatrixTo whether we should force using matrix.to base URL
|
||||||
|
*
|
||||||
|
* @return the created template
|
||||||
|
*/
|
||||||
|
fun createMentionSpanTemplate(type: SpanTemplateType, forceMatrixTo: Boolean = false): String
|
||||||
}
|
}
|
||||||
|
@ -21,5 +21,5 @@ import com.squareup.moshi.JsonClass
|
|||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
data class LocationAsset(
|
data class LocationAsset(
|
||||||
@Json(name = "type") val type: LocationAssetType? = null
|
@Json(name = "type") val type: String? = null
|
||||||
)
|
)
|
||||||
|
@ -16,11 +16,20 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.room.model.message
|
package org.matrix.android.sdk.api.session.room.model.message
|
||||||
|
|
||||||
import com.squareup.moshi.Json
|
/**
|
||||||
import com.squareup.moshi.JsonClass
|
* Define what particular asset is being referred to.
|
||||||
|
* We don't use enum type since it is not limited to a specific set of values.
|
||||||
|
* The way this type should be interpreted in client side is described in
|
||||||
|
* [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||||
|
*/
|
||||||
|
object LocationAssetType {
|
||||||
|
/**
|
||||||
|
* Used for user location sharing.
|
||||||
|
**/
|
||||||
|
const val SELF = "m.self"
|
||||||
|
|
||||||
@JsonClass(generateAdapter = false)
|
/**
|
||||||
enum class LocationAssetType {
|
* Used for pin drop location sharing.
|
||||||
@Json(name = "m.self")
|
**/
|
||||||
SELF
|
const val PIN = "m.pin"
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ data class MessageLocationContent(
|
|||||||
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
@Json(name = "m.relates_to") override val relatesTo: RelationDefaultContent? = null,
|
||||||
@Json(name = "m.new_content") override val newContent: Content? = null,
|
@Json(name = "m.new_content") override val newContent: Content? = null,
|
||||||
/**
|
/**
|
||||||
* See https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md
|
* See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||||
*/
|
*/
|
||||||
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
|
@Json(name = "org.matrix.msc3488.location") val unstableLocationInfo: LocationInfo? = null,
|
||||||
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
|
@Json(name = "m.location") val locationInfo: LocationInfo? = null,
|
||||||
@ -54,10 +54,11 @@ data class MessageLocationContent(
|
|||||||
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
|
@Json(name = "org.matrix.msc1767.text") val unstableText: String? = null,
|
||||||
@Json(name = "m.text") val text: String? = null,
|
@Json(name = "m.text") val text: String? = null,
|
||||||
/**
|
/**
|
||||||
* m.asset defines a generic asset that can be used for location tracking but also in other places like
|
* Defines a generic asset that can be used for location tracking but also in other places like
|
||||||
* inventories, geofencing, checkins/checkouts etc.
|
* inventories, geofencing, checkins/checkouts etc.
|
||||||
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
* It should contain a mandatory namespaced type key defining what particular asset is being referred to.
|
||||||
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
* For the purposes of user location tracking m.self should be used in order to avoid duplicating the mxid.
|
||||||
|
* See [MSC3488](https://github.com/matrix-org/matrix-doc/blob/matthew/location/proposals/3488-location.md)
|
||||||
*/
|
*/
|
||||||
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
|
@Json(name = "org.matrix.msc3488.asset") val unstableLocationAsset: LocationAsset? = null,
|
||||||
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
|
@Json(name = "m.asset") val locationAsset: LocationAsset? = null
|
||||||
|
@ -142,8 +142,9 @@ interface SendService {
|
|||||||
* @param latitude required latitude of the location
|
* @param latitude required latitude of the location
|
||||||
* @param longitude required longitude of the location
|
* @param longitude required longitude of the location
|
||||||
* @param uncertainty Accuracy of the location in meters
|
* @param uncertainty Accuracy of the location in meters
|
||||||
|
* @param isUserLocation indicates whether the location data corresponds to the user location or not
|
||||||
*/
|
*/
|
||||||
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable
|
fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove this failed message from the timeline
|
* Remove this failed message from the timeline
|
||||||
|
@ -97,6 +97,7 @@ internal fun RealmQuery<TimelineEventEntity>.filterEvents(filters: TimelineEvent
|
|||||||
if (filters.filterEdits) {
|
if (filters.filterEdits) {
|
||||||
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
|
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.EDIT)
|
||||||
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
|
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.RESPONSE)
|
||||||
|
not().like(TimelineEventEntityFields.ROOT.CONTENT, TimelineEventFilter.Content.REFERENCE)
|
||||||
}
|
}
|
||||||
if (filters.filterRedacted) {
|
if (filters.filterRedacted) {
|
||||||
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
|
not().like(TimelineEventEntityFields.ROOT.UNSIGNED_DATA, TimelineEventFilter.Unsigned.REDACTED)
|
||||||
|
@ -26,6 +26,7 @@ internal object TimelineEventFilter {
|
|||||||
internal object Content {
|
internal object Content {
|
||||||
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
internal const val EDIT = """{*"m.relates_to"*"rel_type":*"m.replace"*}"""
|
||||||
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
|
internal const val RESPONSE = """{*"m.relates_to"*"rel_type":*"org.matrix.response"*}"""
|
||||||
|
internal const val REFERENCE = """{*"m.relates_to"*"rel_type":*"m.reference"*}"""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -43,4 +43,8 @@ internal class DefaultPermalinkService @Inject constructor(
|
|||||||
override fun getLinkedId(url: String): String? {
|
override fun getLinkedId(url: String): String? {
|
||||||
return permalinkFactory.getLinkedId(url)
|
return permalinkFactory.getLinkedId(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
||||||
|
return permalinkFactory.createMentionSpanTemplate(type, forceMatrixTo)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,10 @@ import org.matrix.android.sdk.api.MatrixPatterns
|
|||||||
import org.matrix.android.sdk.api.session.events.model.Event
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkData
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkParser
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.Companion.MATRIX_TO_URL_BASE
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.HTML
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService.SpanTemplateType.MARKDOWN
|
||||||
import org.matrix.android.sdk.internal.di.UserId
|
import org.matrix.android.sdk.internal.di.UserId
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@ -105,6 +108,23 @@ internal class PermalinkFactory @Inject constructor(
|
|||||||
?.substringBeforeLast("?")
|
?.substringBeforeLast("?")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun createMentionSpanTemplate(type: PermalinkService.SpanTemplateType, forceMatrixTo: Boolean): String {
|
||||||
|
return buildString {
|
||||||
|
when (type) {
|
||||||
|
HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN)
|
||||||
|
MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_BEGIN)
|
||||||
|
}
|
||||||
|
append(baseUrl(forceMatrixTo))
|
||||||
|
if (useClientFormat(forceMatrixTo)) {
|
||||||
|
append(USER_PATH)
|
||||||
|
}
|
||||||
|
when (type) {
|
||||||
|
HTML -> append(MENTION_SPAN_TO_HTML_TEMPLATE_END)
|
||||||
|
MARKDOWN -> append(MENTION_SPAN_TO_MD_TEMPLATE_END)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape '/' in id, because it is used as a separator
|
* Escape '/' in id, because it is used as a separator
|
||||||
*
|
*
|
||||||
@ -147,5 +167,9 @@ internal class PermalinkFactory @Inject constructor(
|
|||||||
private const val ROOM_PATH = "room/"
|
private const val ROOM_PATH = "room/"
|
||||||
private const val USER_PATH = "user/"
|
private const val USER_PATH = "user/"
|
||||||
private const val GROUP_PATH = "group/"
|
private const val GROUP_PATH = "group/"
|
||||||
|
private const val MENTION_SPAN_TO_HTML_TEMPLATE_BEGIN = "<a href=\""
|
||||||
|
private const val MENTION_SPAN_TO_HTML_TEMPLATE_END = "%1\$s\">%2\$s</a>"
|
||||||
|
private const val MENTION_SPAN_TO_MD_TEMPLATE_BEGIN = "[%2\$s]("
|
||||||
|
private const val MENTION_SPAN_TO_MD_TEMPLATE_END = "%1\$s)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,8 +128,8 @@ internal class DefaultSendService @AssistedInject constructor(
|
|||||||
.let { sendEvent(it) }
|
.let { sendEvent(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?): Cancelable {
|
override fun sendLocation(latitude: Double, longitude: Double, uncertainty: Double?, isUserLocation: Boolean): Cancelable {
|
||||||
return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty)
|
return localEchoEventFactory.createLocationEvent(roomId, latitude, longitude, uncertainty, isUserLocation)
|
||||||
.also { createLocalEcho(it) }
|
.also { createLocalEcho(it) }
|
||||||
.let { sendEvent(it) }
|
.let { sendEvent(it) }
|
||||||
}
|
}
|
||||||
|
@ -227,13 +227,15 @@ internal class LocalEchoEventFactory @Inject constructor(
|
|||||||
fun createLocationEvent(roomId: String,
|
fun createLocationEvent(roomId: String,
|
||||||
latitude: Double,
|
latitude: Double,
|
||||||
longitude: Double,
|
longitude: Double,
|
||||||
uncertainty: Double?): Event {
|
uncertainty: Double?,
|
||||||
|
isUserLocation: Boolean): Event {
|
||||||
val geoUri = buildGeoUri(latitude, longitude, uncertainty)
|
val geoUri = buildGeoUri(latitude, longitude, uncertainty)
|
||||||
|
val assetType = if (isUserLocation) LocationAssetType.SELF else LocationAssetType.PIN
|
||||||
val content = MessageLocationContent(
|
val content = MessageLocationContent(
|
||||||
geoUri = geoUri,
|
geoUri = geoUri,
|
||||||
body = geoUri,
|
body = geoUri,
|
||||||
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
|
unstableLocationInfo = LocationInfo(geoUri = geoUri, description = geoUri),
|
||||||
unstableLocationAsset = LocationAsset(type = LocationAssetType.SELF),
|
unstableLocationAsset = LocationAsset(type = assetType),
|
||||||
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
unstableTs = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis()),
|
||||||
unstableText = geoUri
|
unstableText = geoUri
|
||||||
)
|
)
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
package org.matrix.android.sdk.internal.session.room.send.pills
|
package org.matrix.android.sdk.internal.session.room.send.pills
|
||||||
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
|
import org.matrix.android.sdk.api.session.permalinks.PermalinkService
|
||||||
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
import org.matrix.android.sdk.internal.session.displayname.DisplayNameResolver
|
||||||
@ -28,7 +29,8 @@ import javax.inject.Inject
|
|||||||
*/
|
*/
|
||||||
internal class TextPillsUtils @Inject constructor(
|
internal class TextPillsUtils @Inject constructor(
|
||||||
private val mentionLinkSpecComparator: MentionLinkSpecComparator,
|
private val mentionLinkSpecComparator: MentionLinkSpecComparator,
|
||||||
private val displayNameResolver: DisplayNameResolver
|
private val displayNameResolver: DisplayNameResolver,
|
||||||
|
private val permalinkService: PermalinkService
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +38,7 @@ internal class TextPillsUtils @Inject constructor(
|
|||||||
* @return the transformed String or null if no Span found
|
* @return the transformed String or null if no Span found
|
||||||
*/
|
*/
|
||||||
fun processSpecialSpansToHtml(text: CharSequence): String? {
|
fun processSpecialSpansToHtml(text: CharSequence): String? {
|
||||||
return transformPills(text, MENTION_SPAN_TO_HTML_TEMPLATE)
|
return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.HTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -44,7 +46,7 @@ internal class TextPillsUtils @Inject constructor(
|
|||||||
* @return the transformed String or null if no Span found
|
* @return the transformed String or null if no Span found
|
||||||
*/
|
*/
|
||||||
fun processSpecialSpansToMarkdown(text: CharSequence): String? {
|
fun processSpecialSpansToMarkdown(text: CharSequence): String? {
|
||||||
return transformPills(text, MENTION_SPAN_TO_MD_TEMPLATE)
|
return transformPills(text, permalinkService.createMentionSpanTemplate(PermalinkService.SpanTemplateType.MARKDOWN))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun transformPills(text: CharSequence, template: String): String? {
|
private fun transformPills(text: CharSequence, template: String): String? {
|
||||||
@ -108,10 +110,4 @@ internal class TextPillsUtils @Inject constructor(
|
|||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val MENTION_SPAN_TO_HTML_TEMPLATE = "<a href=\"https://matrix.to/#/%1\$s\">%2\$s</a>"
|
|
||||||
|
|
||||||
private const val MENTION_SPAN_TO_MD_TEMPLATE = "[%2\$s](https://matrix.to/#/%1\$s)"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.core.animations
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.ColorInt
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import im.vector.app.R
|
||||||
|
import nl.dionsegijn.konfetti.KonfettiView
|
||||||
|
import nl.dionsegijn.konfetti.models.Shape
|
||||||
|
import nl.dionsegijn.konfetti.models.Size
|
||||||
|
|
||||||
|
fun KonfettiView.play() {
|
||||||
|
val confettiColors = listOf(
|
||||||
|
R.color.palette_azure,
|
||||||
|
R.color.palette_grape,
|
||||||
|
R.color.palette_verde,
|
||||||
|
R.color.palette_polly,
|
||||||
|
R.color.palette_melon,
|
||||||
|
R.color.palette_aqua,
|
||||||
|
R.color.palette_prune,
|
||||||
|
R.color.palette_kiwi
|
||||||
|
)
|
||||||
|
build()
|
||||||
|
.addColors(confettiColors.toColorInt(context))
|
||||||
|
.setDirection(0.0, 359.0)
|
||||||
|
.setSpeed(2f, 5f)
|
||||||
|
.setFadeOutEnabled(true)
|
||||||
|
.setTimeToLive(2000L)
|
||||||
|
.addShapes(Shape.Square, Shape.Circle)
|
||||||
|
.addSizes(Size(12))
|
||||||
|
.setPosition(-50f, width + 50f, -50f, -50f)
|
||||||
|
.streamFor(150, 3000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ColorInt
|
||||||
|
private fun List<Int>.toColorInt(context: Context) = map { ContextCompat.getColor(context, it) }
|
@ -103,6 +103,7 @@ import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseDisplayNameFragm
|
|||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthChooseProfilePictureFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthGenericTextInputFormFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthLoginFragment
|
||||||
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthPersonalizationCompleteFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordMailConfirmationFragment
|
||||||
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
|
import im.vector.app.features.onboarding.ftueauth.FtueAuthResetPasswordSuccessFragment
|
||||||
@ -491,6 +492,11 @@ interface FragmentModule {
|
|||||||
@FragmentKey(FtueAuthChooseProfilePictureFragment::class)
|
@FragmentKey(FtueAuthChooseProfilePictureFragment::class)
|
||||||
fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment
|
fun bindFtueAuthChooseProfilePictureFragment(fragment: FtueAuthChooseProfilePictureFragment): Fragment
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@FragmentKey(FtueAuthPersonalizationCompleteFragment::class)
|
||||||
|
fun bindFtueAuthPersonalizationCompleteFragment(fragment: FtueAuthPersonalizationCompleteFragment): Fragment
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@FragmentKey(UserListFragment::class)
|
@FragmentKey(UserListFragment::class)
|
||||||
|
@ -20,7 +20,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.Color
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -68,6 +67,7 @@ import com.airbnb.mvrx.withState
|
|||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.vanniktech.emoji.EmojiPopup
|
import com.vanniktech.emoji.EmojiPopup
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.animations.play
|
||||||
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
import im.vector.app.core.dialogs.ConfirmationDialogBuilder
|
||||||
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
import im.vector.app.core.dialogs.GalleryOrCameraDialogHelper
|
||||||
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
|
||||||
@ -204,8 +204,6 @@ import kotlinx.coroutines.flow.map
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import nl.dionsegijn.konfetti.models.Shape
|
|
||||||
import nl.dionsegijn.konfetti.models.Size
|
|
||||||
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
import org.billcarsonfr.jsonviewer.JSonViewerDialog
|
||||||
import org.commonmark.parser.Parser
|
import org.commonmark.parser.Parser
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -563,16 +561,7 @@ class TimelineFragment @Inject constructor(
|
|||||||
when (chatEffect) {
|
when (chatEffect) {
|
||||||
ChatEffect.CONFETTI -> {
|
ChatEffect.CONFETTI -> {
|
||||||
views.viewKonfetti.isVisible = true
|
views.viewKonfetti.isVisible = true
|
||||||
views.viewKonfetti.build()
|
views.viewKonfetti.play()
|
||||||
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA)
|
|
||||||
.setDirection(0.0, 359.0)
|
|
||||||
.setSpeed(2f, 5f)
|
|
||||||
.setFadeOutEnabled(true)
|
|
||||||
.setTimeToLive(2000L)
|
|
||||||
.addShapes(Shape.Square, Shape.Circle)
|
|
||||||
.addSizes(Size(12))
|
|
||||||
.setPosition(-50f, views.viewKonfetti.width + 50f, -50f, -50f)
|
|
||||||
.streamFor(150, 3000L)
|
|
||||||
}
|
}
|
||||||
ChatEffect.SNOWFALL -> {
|
ChatEffect.SNOWFALL -> {
|
||||||
views.viewSnowFall.isVisible = true
|
views.viewSnowFall.isVisible = true
|
||||||
|
@ -123,7 +123,7 @@ class LocationPreviewFragment @Inject constructor(
|
|||||||
views.mapView.render(
|
views.mapView.render(
|
||||||
MapState(
|
MapState(
|
||||||
zoomOnlyOnce = true,
|
zoomOnlyOnce = true,
|
||||||
pinLocationData = location,
|
userLocationData = location,
|
||||||
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
pinId = args.locationOwnerId ?: DEFAULT_PIN_ID,
|
||||||
pinDrawable = pinDrawable
|
pinDrawable = pinDrawable
|
||||||
)
|
)
|
||||||
|
@ -19,5 +19,8 @@ package im.vector.app.features.location
|
|||||||
import im.vector.app.core.platform.VectorViewModelAction
|
import im.vector.app.core.platform.VectorViewModelAction
|
||||||
|
|
||||||
sealed class LocationSharingAction : VectorViewModelAction {
|
sealed class LocationSharingAction : VectorViewModelAction {
|
||||||
object OnShareLocation : LocationSharingAction()
|
object CurrentUserLocationSharing : LocationSharingAction()
|
||||||
|
data class PinnedLocationSharing(val locationData: LocationData?) : LocationSharingAction()
|
||||||
|
data class LocationTargetChange(val locationData: LocationData) : LocationSharingAction()
|
||||||
|
object ZoomToUserLocation : LocationSharingAction()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
@ -44,7 +45,7 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
private val urlMapProvider: UrlMapProvider,
|
private val urlMapProvider: UrlMapProvider,
|
||||||
private val avatarRenderer: AvatarRenderer,
|
private val avatarRenderer: AvatarRenderer,
|
||||||
private val matrixItemColorProvider: MatrixItemColorProvider
|
private val matrixItemColorProvider: MatrixItemColorProvider
|
||||||
) : VectorBaseFragment<FragmentLocationSharingBinding>() {
|
) : VectorBaseFragment<FragmentLocationSharingBinding>(), LocationTargetChangeListener {
|
||||||
|
|
||||||
private val viewModel: LocationSharingViewModel by fragmentViewModel()
|
private val viewModel: LocationSharingViewModel by fragmentViewModel()
|
||||||
|
|
||||||
@ -64,15 +65,20 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
views.mapView.onCreate(savedInstanceState)
|
views.mapView.onCreate(savedInstanceState)
|
||||||
|
|
||||||
lifecycleScope.launchWhenCreated {
|
lifecycleScope.launchWhenCreated {
|
||||||
views.mapView.initialize(urlMapProvider.getMapUrl())
|
views.mapView.initialize(
|
||||||
|
url = urlMapProvider.getMapUrl(),
|
||||||
|
locationTargetChangeListener = this@LocationSharingFragment
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initLocateButton()
|
||||||
initOptionsPicker()
|
initOptionsPicker()
|
||||||
|
|
||||||
viewModel.observeViewEvents {
|
viewModel.observeViewEvents {
|
||||||
when (it) {
|
when (it) {
|
||||||
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
LocationSharingViewEvents.LocationNotAvailableError -> handleLocationNotAvailableError()
|
||||||
LocationSharingViewEvents.Close -> activity?.finish()
|
LocationSharingViewEvents.Close -> activity?.finish()
|
||||||
|
is LocationSharingViewEvents.ZoomToUserLocation -> handleZoomToUserLocationEvent(it)
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,10 +119,17 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLocationTargetChange(target: LocationData) {
|
||||||
|
viewModel.handle(LocationSharingAction.LocationTargetChange(target))
|
||||||
|
}
|
||||||
|
|
||||||
override fun invalidate() = withState(viewModel) { state ->
|
override fun invalidate() = withState(viewModel) { state ->
|
||||||
views.mapView.render(state.toMapState())
|
updateMap(state)
|
||||||
views.shareLocationGpsLoading.isGone = state.lastKnownLocation != null
|
|
||||||
updateUserAvatar(state.userItem)
|
updateUserAvatar(state.userItem)
|
||||||
|
if (state.locationTargetDrawable != null) {
|
||||||
|
updateLocationTargetPin(state.locationTargetDrawable)
|
||||||
|
}
|
||||||
|
views.shareLocationGpsLoading.isGone = state.lastKnownUserLocation != null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleLocationNotAvailableError() {
|
private fun handleLocationNotAvailableError() {
|
||||||
@ -130,21 +143,52 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initLocateButton() {
|
||||||
|
views.mapView.locateButton.setOnClickListener {
|
||||||
|
viewModel.handle(LocationSharingAction.ZoomToUserLocation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocationEvent(event: LocationSharingViewEvents.ZoomToUserLocation) {
|
||||||
|
views.mapView.zoomToLocation(event.userLocation.latitude, event.userLocation.longitude)
|
||||||
|
}
|
||||||
|
|
||||||
private fun initOptionsPicker() {
|
private fun initOptionsPicker() {
|
||||||
// TODO
|
// set no option at start
|
||||||
// change the options dynamically depending on the current chosen location
|
views.shareLocationOptionsPicker.render()
|
||||||
views.shareLocationOptionsPicker.render(LocationSharingOption.USER_CURRENT)
|
|
||||||
views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
|
views.shareLocationOptionsPicker.optionPinned.debouncedClicks {
|
||||||
// TODO
|
val targetLocation = views.mapView.getLocationOfMapCenter()
|
||||||
|
viewModel.handle(LocationSharingAction.PinnedLocationSharing(targetLocation))
|
||||||
}
|
}
|
||||||
views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
|
views.shareLocationOptionsPicker.optionUserCurrent.debouncedClicks {
|
||||||
viewModel.handle(LocationSharingAction.OnShareLocation)
|
viewModel.handle(LocationSharingAction.CurrentUserLocationSharing)
|
||||||
}
|
}
|
||||||
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
|
views.shareLocationOptionsPicker.optionUserLive.debouncedClicks {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateMap(state: LocationSharingViewState) {
|
||||||
|
// first, update the options view
|
||||||
|
when (state.areTargetAndUserLocationEqual) {
|
||||||
|
// TODO activate USER_LIVE option when implemented
|
||||||
|
true -> views.shareLocationOptionsPicker.render(
|
||||||
|
LocationSharingOption.USER_CURRENT
|
||||||
|
)
|
||||||
|
false -> views.shareLocationOptionsPicker.render(
|
||||||
|
LocationSharingOption.PINNED
|
||||||
|
)
|
||||||
|
else -> views.shareLocationOptionsPicker.render()
|
||||||
|
}
|
||||||
|
// then, update the map using the height of the options view after it has been rendered
|
||||||
|
views.shareLocationOptionsPicker.post {
|
||||||
|
val mapState = state
|
||||||
|
.toMapState()
|
||||||
|
.copy(logoMarginBottom = views.shareLocationOptionsPicker.height)
|
||||||
|
views.mapView.render(mapState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun updateUserAvatar(userItem: MatrixItem.UserItem?) {
|
private fun updateUserAvatar(userItem: MatrixItem.UserItem?) {
|
||||||
userItem?.takeUnless { hasRenderedUserAvatar }
|
userItem?.takeUnless { hasRenderedUserAvatar }
|
||||||
?.let {
|
?.let {
|
||||||
@ -154,4 +198,8 @@ class LocationSharingFragment @Inject constructor(
|
|||||||
views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor)
|
views.shareLocationOptionsPicker.optionUserCurrent.setIconBackgroundTint(tintColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun updateLocationTargetPin(drawable: Drawable) {
|
||||||
|
views.shareLocationPin.setImageDrawable(drawable)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,4 +21,5 @@ import im.vector.app.core.platform.VectorViewEvents
|
|||||||
sealed class LocationSharingViewEvents : VectorViewEvents {
|
sealed class LocationSharingViewEvents : VectorViewEvents {
|
||||||
object Close : LocationSharingViewEvents()
|
object Close : LocationSharingViewEvents()
|
||||||
object LocationNotAvailableError : LocationSharingViewEvents()
|
object LocationNotAvailableError : LocationSharingViewEvents()
|
||||||
|
data class ZoomToUserLocation(val userLocation: LocationData) : LocationSharingViewEvents()
|
||||||
}
|
}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
import com.airbnb.mvrx.MavericksViewModelFactory
|
import com.airbnb.mvrx.MavericksViewModelFactory
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
@ -25,18 +26,36 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
|||||||
import im.vector.app.core.extensions.exhaustive
|
import im.vector.app.core.extensions.exhaustive
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.LocationPinProvider
|
||||||
|
import im.vector.app.features.location.domain.usecase.CompareLocationsUseCase
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.lastOrNull
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.util.toMatrixItem
|
import org.matrix.android.sdk.api.util.toMatrixItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sampling period to compare target location and user location.
|
||||||
|
*/
|
||||||
|
private const val TARGET_LOCATION_CHANGE_SAMPLING_PERIOD_IN_MS = 100L
|
||||||
|
|
||||||
class LocationSharingViewModel @AssistedInject constructor(
|
class LocationSharingViewModel @AssistedInject constructor(
|
||||||
@Assisted private val initialState: LocationSharingViewState,
|
@Assisted private val initialState: LocationSharingViewState,
|
||||||
private val locationTracker: LocationTracker,
|
private val locationTracker: LocationTracker,
|
||||||
private val locationPinProvider: LocationPinProvider,
|
private val locationPinProvider: LocationPinProvider,
|
||||||
private val session: Session
|
private val session: Session,
|
||||||
|
private val compareLocationsUseCase: CompareLocationsUseCase
|
||||||
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
|
) : VectorViewModel<LocationSharingViewState, LocationSharingAction, LocationSharingViewEvents>(initialState), LocationTracker.Callback {
|
||||||
|
|
||||||
private val room = session.getRoom(initialState.roomId)!!
|
private val room = session.getRoom(initialState.roomId)!!
|
||||||
|
|
||||||
|
private val locationTargetFlow = MutableSharedFlow<LocationData>()
|
||||||
|
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
|
interface Factory : MavericksAssistedViewModelFactory<LocationSharingViewModel, LocationSharingViewState> {
|
||||||
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
override fun create(initialState: LocationSharingViewState): LocationSharingViewModel
|
||||||
@ -47,23 +66,49 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
init {
|
init {
|
||||||
locationTracker.start(this)
|
locationTracker.start(this)
|
||||||
setUserItem()
|
setUserItem()
|
||||||
createPin()
|
updatePin()
|
||||||
|
compareTargetAndUserLocation()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setUserItem() {
|
private fun setUserItem() {
|
||||||
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
|
setState { copy(userItem = session.getUser(session.myUserId)?.toMatrixItem()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPin() {
|
private fun updatePin(isUserPin: Boolean? = true) {
|
||||||
locationPinProvider.create(session.myUserId) {
|
if (isUserPin.orFalse()) {
|
||||||
setState {
|
locationPinProvider.create(userId = session.myUserId) {
|
||||||
copy(
|
updatePinDrawableInState(it)
|
||||||
pinDrawable = 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() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
locationTracker.stop()
|
locationTracker.stop()
|
||||||
@ -71,16 +116,28 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
override fun handle(action: LocationSharingAction) {
|
override fun handle(action: LocationSharingAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
LocationSharingAction.OnShareLocation -> handleShareLocation()
|
LocationSharingAction.CurrentUserLocationSharing -> handleCurrentUserLocationSharingAction()
|
||||||
|
is LocationSharingAction.PinnedLocationSharing -> handlePinnedLocationSharingAction(action)
|
||||||
|
is LocationSharingAction.LocationTargetChange -> handleLocationTargetChangeAction(action)
|
||||||
|
LocationSharingAction.ZoomToUserLocation -> handleZoomToUserLocationAction()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleShareLocation() = withState { state ->
|
private fun handleCurrentUserLocationSharingAction() = withState { state ->
|
||||||
state.lastKnownLocation?.let { location ->
|
shareLocation(state.lastKnownUserLocation, isUserLocation = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handlePinnedLocationSharingAction(action: LocationSharingAction.PinnedLocationSharing) {
|
||||||
|
shareLocation(action.locationData, isUserLocation = false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareLocation(locationData: LocationData?, isUserLocation: Boolean) {
|
||||||
|
locationData?.let { location ->
|
||||||
room.sendLocation(
|
room.sendLocation(
|
||||||
latitude = location.latitude,
|
latitude = location.latitude,
|
||||||
longitude = location.longitude,
|
longitude = location.longitude,
|
||||||
uncertainty = location.uncertainty
|
uncertainty = location.uncertainty,
|
||||||
|
isUserLocation = isUserLocation
|
||||||
)
|
)
|
||||||
_viewEvents.post(LocationSharingViewEvents.Close)
|
_viewEvents.post(LocationSharingViewEvents.Close)
|
||||||
} ?: run {
|
} ?: run {
|
||||||
@ -88,9 +145,27 @@ class LocationSharingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleLocationTargetChangeAction(action: LocationSharingAction.LocationTargetChange) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
locationTargetFlow.emit(action.locationData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleZoomToUserLocationAction() = withState { state ->
|
||||||
|
state.lastKnownUserLocation?.let { location ->
|
||||||
|
_viewEvents.post(LocationSharingViewEvents.ZoomToUserLocation(location))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLocationUpdate(locationData: LocationData) {
|
override fun onLocationUpdate(locationData: LocationData) {
|
||||||
setState {
|
setState {
|
||||||
copy(lastKnownLocation = locationData)
|
copy(lastKnownUserLocation = locationData)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
// recompute location comparison using last received target location
|
||||||
|
locationTargetFlow.lastOrNull()?.let {
|
||||||
|
locationTargetFlow.emit(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ import android.graphics.drawable.Drawable
|
|||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import com.airbnb.mvrx.MavericksState
|
import com.airbnb.mvrx.MavericksState
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import org.matrix.android.sdk.api.extensions.orTrue
|
||||||
import org.matrix.android.sdk.api.util.MatrixItem
|
import org.matrix.android.sdk.api.util.MatrixItem
|
||||||
|
|
||||||
enum class LocationSharingMode(@StringRes val titleRes: Int) {
|
enum class LocationSharingMode(@StringRes val titleRes: Int) {
|
||||||
@ -31,8 +32,9 @@ data class LocationSharingViewState(
|
|||||||
val roomId: String,
|
val roomId: String,
|
||||||
val mode: LocationSharingMode,
|
val mode: LocationSharingMode,
|
||||||
val userItem: MatrixItem.UserItem? = null,
|
val userItem: MatrixItem.UserItem? = null,
|
||||||
val lastKnownLocation: LocationData? = null,
|
val areTargetAndUserLocationEqual: Boolean? = null,
|
||||||
val pinDrawable: Drawable? = null
|
val lastKnownUserLocation: LocationData? = null,
|
||||||
|
val locationTargetDrawable: Drawable? = null
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(locationSharingArgs: LocationSharingArgs) : this(
|
constructor(locationSharingArgs: LocationSharingArgs) : this(
|
||||||
@ -43,7 +45,9 @@ data class LocationSharingViewState(
|
|||||||
|
|
||||||
fun LocationSharingViewState.toMapState() = MapState(
|
fun LocationSharingViewState.toMapState() = MapState(
|
||||||
zoomOnlyOnce = true,
|
zoomOnlyOnce = true,
|
||||||
pinLocationData = lastKnownLocation,
|
userLocationData = lastKnownUserLocation,
|
||||||
pinId = DEFAULT_PIN_ID,
|
pinId = DEFAULT_PIN_ID,
|
||||||
pinDrawable = pinDrawable
|
pinDrawable = null,
|
||||||
|
// show the map pin only when target location and user location are not equal
|
||||||
|
showPin = areTargetAndUserLocationEqual.orTrue().not()
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
}
|
@ -17,10 +17,13 @@
|
|||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.annotation.Px
|
||||||
|
|
||||||
data class MapState(
|
data class MapState(
|
||||||
val zoomOnlyOnce: Boolean,
|
val zoomOnlyOnce: Boolean,
|
||||||
val pinLocationData: LocationData? = null,
|
val userLocationData: LocationData? = null,
|
||||||
val pinId: String,
|
val pinId: String,
|
||||||
val pinDrawable: Drawable? = null
|
val pinDrawable: Drawable? = null,
|
||||||
|
val showPin: Boolean = true,
|
||||||
|
@Px val logoMarginBottom: Int = 0
|
||||||
)
|
)
|
||||||
|
@ -17,7 +17,14 @@
|
|||||||
package im.vector.app.features.location
|
package im.vector.app.features.location
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.res.TypedArray
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.view.marginBottom
|
||||||
|
import androidx.core.view.marginTop
|
||||||
|
import androidx.core.view.updateLayoutParams
|
||||||
import com.mapbox.mapboxsdk.camera.CameraPosition
|
import com.mapbox.mapboxsdk.camera.CameraPosition
|
||||||
import com.mapbox.mapboxsdk.geometry.LatLng
|
import com.mapbox.mapboxsdk.geometry.LatLng
|
||||||
import com.mapbox.mapboxsdk.maps.MapView
|
import com.mapbox.mapboxsdk.maps.MapView
|
||||||
@ -26,6 +33,7 @@ import com.mapbox.mapboxsdk.maps.Style
|
|||||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager
|
||||||
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions
|
||||||
import com.mapbox.mapboxsdk.style.layers.Property
|
import com.mapbox.mapboxsdk.style.layers.Property
|
||||||
|
import im.vector.app.R
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
class MapTilerMapView @JvmOverloads constructor(
|
class MapTilerMapView @JvmOverloads constructor(
|
||||||
@ -42,24 +50,100 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||||||
val style: Style
|
val style: Style
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private val userLocationDrawable by lazy {
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.ic_location_user)
|
||||||
|
}
|
||||||
|
val locateButton by lazy { createLocateButton() }
|
||||||
private var mapRefs: MapRefs? = null
|
private var mapRefs: MapRefs? = null
|
||||||
private var initZoomDone = false
|
private var initZoomDone = false
|
||||||
|
private var showLocationButton = false
|
||||||
|
|
||||||
|
init {
|
||||||
|
context.theme.obtainStyledAttributes(
|
||||||
|
attrs,
|
||||||
|
R.styleable.MapTilerMapView,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
).run {
|
||||||
|
try {
|
||||||
|
setLocateButtonVisibility(this)
|
||||||
|
} finally {
|
||||||
|
recycle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLocateButtonVisibility(typedArray: TypedArray) {
|
||||||
|
showLocationButton = typedArray.getBoolean(R.styleable.MapTilerMapView_showLocateButton, false)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For location fragments
|
* For location fragments
|
||||||
*/
|
*/
|
||||||
fun initialize(url: String) {
|
fun initialize(
|
||||||
|
url: String,
|
||||||
|
locationTargetChangeListener: LocationTargetChangeListener? = null
|
||||||
|
) {
|
||||||
Timber.d("## Location: initialize")
|
Timber.d("## Location: initialize")
|
||||||
getMapAsync { map ->
|
getMapAsync { map ->
|
||||||
map.setStyle(url) { style ->
|
initMapStyle(map, url)
|
||||||
mapRefs = MapRefs(
|
initLocateButton(map)
|
||||||
map,
|
notifyLocationOfMapCenter(locationTargetChangeListener)
|
||||||
SymbolManager(this, map, style),
|
listenCameraMove(map, locationTargetChangeListener)
|
||||||
style
|
}
|
||||||
)
|
}
|
||||||
pendingState?.let { render(it) }
|
|
||||||
pendingState = null
|
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<MarginLayoutParams> {
|
||||||
|
val marginHorizontal = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_locate_button_margin_horizontal)
|
||||||
|
val marginVertical = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_locate_button_margin_vertical)
|
||||||
|
setMargins(marginHorizontal, marginVertical, marginHorizontal, marginVertical)
|
||||||
|
}
|
||||||
|
updateLayoutParams<LayoutParams> {
|
||||||
|
gravity = Gravity.TOP or Gravity.END
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun adjustCompassButton(map: MapboxMap) {
|
||||||
|
locateButton.post {
|
||||||
|
val marginTop = locateButton.height + locateButton.marginTop + locateButton.marginBottom
|
||||||
|
val marginRight = context.resources.getDimensionPixelOffset(R.dimen.location_sharing_compass_button_margin_horizontal)
|
||||||
|
map.uiSettings.setCompassMargins(0, marginTop, marginRight, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun listenCameraMove(map: MapboxMap, locationTargetChangeListener: LocationTargetChangeListener?) {
|
||||||
|
map.addOnCameraMoveListener {
|
||||||
|
notifyLocationOfMapCenter(locationTargetChangeListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyLocationOfMapCenter(locationTargetChangeListener: LocationTargetChangeListener?) {
|
||||||
|
getLocationOfMapCenter()?.let { target ->
|
||||||
|
locationTargetChangeListener?.onLocationTargetChange(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,34 +152,48 @@ class MapTilerMapView @JvmOverloads constructor(
|
|||||||
pendingState = state
|
pendingState = state
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pinDrawable?.let { pinDrawable ->
|
safeMapRefs.map.uiSettings.setLogoMargins(0, 0, 0, state.logoMarginBottom)
|
||||||
|
|
||||||
|
val pinDrawable = state.pinDrawable ?: userLocationDrawable
|
||||||
|
pinDrawable?.let { drawable ->
|
||||||
if (!safeMapRefs.style.isFullyLoaded ||
|
if (!safeMapRefs.style.isFullyLoaded ||
|
||||||
safeMapRefs.style.getImage(state.pinId) == null) {
|
safeMapRefs.style.getImage(state.pinId) == null) {
|
||||||
safeMapRefs.style.addImage(state.pinId, pinDrawable)
|
safeMapRefs.style.addImage(state.pinId, drawable)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state.pinLocationData?.let { locationData ->
|
state.userLocationData?.let { locationData ->
|
||||||
if (!initZoomDone || !state.zoomOnlyOnce) {
|
if (!initZoomDone || !state.zoomOnlyOnce) {
|
||||||
zoomToLocation(locationData.latitude, locationData.longitude)
|
zoomToLocation(locationData.latitude, locationData.longitude)
|
||||||
initZoomDone = true
|
initZoomDone = true
|
||||||
}
|
}
|
||||||
|
|
||||||
safeMapRefs.symbolManager.deleteAll()
|
safeMapRefs.symbolManager.deleteAll()
|
||||||
safeMapRefs.symbolManager.create(
|
if (pinDrawable != null && state.showPin) {
|
||||||
SymbolOptions()
|
safeMapRefs.symbolManager.create(
|
||||||
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
SymbolOptions()
|
||||||
.withIconImage(state.pinId)
|
.withLatLng(LatLng(locationData.latitude, locationData.longitude))
|
||||||
.withIconAnchor(Property.ICON_ANCHOR_BOTTOM)
|
.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")
|
Timber.d("## Location: zoomToLocation")
|
||||||
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
|
mapRefs?.map?.cameraPosition = CameraPosition.Builder()
|
||||||
.target(LatLng(latitude, longitude))
|
.target(LatLng(latitude, longitude))
|
||||||
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
.zoom(INITIAL_MAP_ZOOM_IN_PREVIEW)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocationOfMapCenter(): LocationData? =
|
||||||
|
mapRefs?.map?.cameraPosition?.target?.let { target ->
|
||||||
|
LocationData(
|
||||||
|
latitude = target.latitude,
|
||||||
|
longitude = target.longitude,
|
||||||
|
uncertainty = null
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,7 @@ import org.matrix.android.sdk.api.session.events.model.Event
|
|||||||
import org.matrix.android.sdk.api.session.events.model.EventType
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
import org.matrix.android.sdk.api.session.events.model.isEdition
|
import org.matrix.android.sdk.api.session.events.model.isEdition
|
||||||
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
import org.matrix.android.sdk.api.session.events.model.isImageMessage
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.supportsNotification
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||||
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
|
||||||
@ -94,7 +95,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
|
suspend fun resolveInMemoryEvent(session: Session, event: Event, canBeReplaced: Boolean): NotifiableEvent? {
|
||||||
if (event.getClearType() != EventType.MESSAGE) return null
|
if (!event.supportsNotification()) return null
|
||||||
|
|
||||||
// Ignore message edition
|
// Ignore message edition
|
||||||
if (event.isEdition()) return null
|
if (event.isEdition()) return null
|
||||||
@ -153,7 +154,8 @@ class NotifiableEventResolver @Inject constructor(
|
|||||||
event.attemptToDecryptIfNeeded(session)
|
event.attemptToDecryptIfNeeded(session)
|
||||||
// only convert encrypted messages to NotifiableMessageEvents
|
// only convert encrypted messages to NotifiableMessageEvents
|
||||||
when (event.root.getClearType()) {
|
when (event.root.getClearType()) {
|
||||||
EventType.MESSAGE -> {
|
EventType.MESSAGE,
|
||||||
|
in EventType.POLL_START -> {
|
||||||
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
|
val body = displayableEventFormatter.format(event, isDm = room.roomSummary()?.isDirect.orFalse(), appendAuthor = false).toString()
|
||||||
val roomName = room.roomSummary()?.displayName ?: ""
|
val roomName = room.roomSummary()?.displayName ?: ""
|
||||||
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
val senderDisplayName = event.senderInfo.disambiguatedDisplayName
|
||||||
@ -185,7 +187,7 @@ class NotifiableEventResolver @Inject constructor(
|
|||||||
soundName = null
|
soundName = null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import android.view.View
|
|||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.animations.play
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
|
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
@ -33,6 +34,8 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
|
|||||||
private val activeSessionHolder: ActiveSessionHolder
|
private val activeSessionHolder: ActiveSessionHolder
|
||||||
) : AbstractFtueAuthFragment<FragmentFtueAccountCreatedBinding>() {
|
) : AbstractFtueAuthFragment<FragmentFtueAccountCreatedBinding>() {
|
||||||
|
|
||||||
|
private var hasPlayedConfetti = false
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAccountCreatedBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueAccountCreatedBinding {
|
||||||
return FragmentFtueAccountCreatedBinding.inflate(inflater, container, false)
|
return FragmentFtueAccountCreatedBinding.inflate(inflater, container, false)
|
||||||
}
|
}
|
||||||
@ -53,6 +56,12 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
|
|||||||
val canPersonalize = state.personalizationState.supportsPersonalization()
|
val canPersonalize = state.personalizationState.supportsPersonalization()
|
||||||
views.personalizeButtonGroup.isVisible = canPersonalize
|
views.personalizeButtonGroup.isVisible = canPersonalize
|
||||||
views.takeMeHomeButtonGroup.isVisible = !canPersonalize
|
views.takeMeHomeButtonGroup.isVisible = !canPersonalize
|
||||||
|
|
||||||
|
if (!hasPlayedConfetti && !canPersonalize) {
|
||||||
|
hasPlayedConfetti = true
|
||||||
|
views.viewKonfetti.isVisible = true
|
||||||
|
views.viewKonfetti.play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resetViewModel() {
|
override fun resetViewModel() {
|
||||||
|
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2021 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package im.vector.app.features.onboarding.ftueauth
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import im.vector.app.core.animations.play
|
||||||
|
import im.vector.app.databinding.FragmentFtuePersonalizationCompleteBinding
|
||||||
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class FtueAuthPersonalizationCompleteFragment @Inject constructor() : AbstractFtueAuthFragment<FragmentFtuePersonalizationCompleteBinding>() {
|
||||||
|
|
||||||
|
private var hasPlayedConfetti = false
|
||||||
|
|
||||||
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtuePersonalizationCompleteBinding {
|
||||||
|
return FragmentFtuePersonalizationCompleteBinding.inflate(inflater, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
setupViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupViews() {
|
||||||
|
views.personalizationCompleteCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
||||||
|
|
||||||
|
if (!hasPlayedConfetti) {
|
||||||
|
hasPlayedConfetti = true
|
||||||
|
views.viewKonfetti.isVisible = true
|
||||||
|
views.viewKonfetti.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun resetViewModel() {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBackPressed(toolbarButton: Boolean): Boolean {
|
||||||
|
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
@ -227,7 +227,7 @@ class FtueAuthVariant(
|
|||||||
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
|
OnboardingViewEvents.OnChooseDisplayName -> onChooseDisplayName()
|
||||||
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
|
OnboardingViewEvents.OnTakeMeHome -> navigateToHome(createdAccount = true)
|
||||||
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
|
OnboardingViewEvents.OnChooseProfilePicture -> onChooseProfilePicture()
|
||||||
OnboardingViewEvents.OnPersonalizationComplete -> navigateToHome(createdAccount = true)
|
OnboardingViewEvents.OnPersonalizationComplete -> onPersonalizationComplete()
|
||||||
OnboardingViewEvents.OnBack -> activity.popBackstack()
|
OnboardingViewEvents.OnBack -> activity.popBackstack()
|
||||||
}.exhaustive
|
}.exhaustive
|
||||||
}
|
}
|
||||||
@ -393,7 +393,8 @@ class FtueAuthVariant(
|
|||||||
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
activity.replaceFragment(
|
activity.replaceFragment(
|
||||||
views.loginFragmentContainer,
|
views.loginFragmentContainer,
|
||||||
FtueAuthAccountCreatedFragment::class.java
|
FtueAuthAccountCreatedFragment::class.java,
|
||||||
|
useCustomAnimation = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -416,4 +417,13 @@ class FtueAuthVariant(
|
|||||||
option = commonOption
|
option = commonOption
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onPersonalizationComplete() {
|
||||||
|
activity.supportFragmentManager.popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE)
|
||||||
|
activity.replaceFragment(
|
||||||
|
views.loginFragmentContainer,
|
||||||
|
FtueAuthPersonalizationCompleteFragment::class.java,
|
||||||
|
useCustomAnimation = true
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
17
vector/src/main/res/drawable/btn_locate.xml
Normal file
17
vector/src/main/res/drawable/btn_locate.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/palette_white" />
|
||||||
|
<corners android:radius="4dp" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:drawable="?selectableItemBackground" />
|
||||||
|
<item
|
||||||
|
android:bottom="8dp"
|
||||||
|
android:drawable="@drawable/ic_locate"
|
||||||
|
android:left="8dp"
|
||||||
|
android:right="8dp"
|
||||||
|
android:top="8dp" />
|
||||||
|
</layer-list>
|
18
vector/src/main/res/drawable/ic_celebration.xml
Normal file
18
vector/src/main/res/drawable/ic_celebration.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="70dp"
|
||||||
|
android:height="70dp"
|
||||||
|
android:viewportWidth="70"
|
||||||
|
android:viewportHeight="70">
|
||||||
|
<path
|
||||||
|
android:pathData="M21,23L19,27L23,25L27,27L25,23L27,19L23,21L19,19L21,23Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#FF0000"
|
||||||
|
android:strokeColor="#FF0000"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M35.653,41.423L38.538,50.076L41.422,41.423L50.076,38.538L41.422,35.654L38.538,27L35.653,35.654L27,38.538L35.653,41.423Z"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="2"
|
||||||
|
android:fillColor="#FF0000"
|
||||||
|
android:strokeColor="#FF0000"/>
|
||||||
|
</vector>
|
9
vector/src/main/res/drawable/ic_locate.xml
Normal file
9
vector/src/main/res/drawable/ic_locate.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="22dp"
|
||||||
|
android:height="22dp"
|
||||||
|
android:viewportWidth="22"
|
||||||
|
android:viewportHeight="22">
|
||||||
|
<path
|
||||||
|
android:pathData="M11,7C8.79,7 7,8.79 7,11C7,13.21 8.79,15 11,15C13.21,15 15,13.21 15,11C15,8.79 13.21,7 11,7ZM19.94,10C19.48,5.83 16.17,2.52 12,2.06V1C12,0.45 11.55,0 11,0C10.45,0 10,0.45 10,1V2.06C5.83,2.52 2.52,5.83 2.06,10H1C0.45,10 0,10.45 0,11C0,11.55 0.45,12 1,12H2.06C2.52,16.17 5.83,19.48 10,19.94V21C10,21.55 10.45,22 11,22C11.55,22 12,21.55 12,21V19.94C16.17,19.48 19.48,16.17 19.94,12H21C21.55,12 22,11.55 22,11C22,10.45 21.55,10 21,10H19.94ZM11,18C7.13,18 4,14.87 4,11C4,7.13 7.13,4 11,4C14.87,4 18,7.13 18,11C18,14.87 14.87,18 11,18Z"
|
||||||
|
android:fillColor="#0DBD8B"/>
|
||||||
|
</vector>
|
14
vector/src/main/res/drawable/ic_location_user.xml
Normal file
14
vector/src/main/res/drawable/ic_location_user.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<size
|
||||||
|
android:width="13dp"
|
||||||
|
android:height="13dp" />
|
||||||
|
<solid android:color="?colorPrimary" />
|
||||||
|
<stroke
|
||||||
|
android:width="2dp"
|
||||||
|
android:color="@color/palette_white" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
10
vector/src/main/res/drawable/ic_user_fg.xml
Normal file
10
vector/src/main/res/drawable/ic_user_fg.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="70dp"
|
||||||
|
android:height="70dp"
|
||||||
|
android:viewportWidth="70"
|
||||||
|
android:viewportHeight="70">
|
||||||
|
<path
|
||||||
|
android:pathData="M35,36.742C40.771,36.742 45.45,31.673 45.45,25.421C45.45,19.169 40.771,14.1 35,14.1C29.229,14.1 24.55,19.169 24.55,25.421C24.55,31.673 29.229,36.742 35,36.742ZM35,62.867C42.531,62.867 49.364,59.879 54.379,55.025C51.278,47.368 43.77,41.967 35,41.967C26.23,41.967 18.722,47.368 15.621,55.025C20.636,59.879 27.469,62.867 35,62.867Z"
|
||||||
|
android:fillColor="#FF0000"
|
||||||
|
android:fillType="evenOdd"/>
|
||||||
|
</vector>
|
@ -6,6 +6,12 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?colorSecondary">
|
android:background="?colorSecondary">
|
||||||
|
|
||||||
|
<nl.dionsegijn.konfetti.KonfettiView
|
||||||
|
android:id="@+id/viewKonfetti"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline
|
<androidx.constraintlayout.widget.Guideline
|
||||||
android:id="@+id/ftueAuthGutterStart"
|
android:id="@+id/ftueAuthGutterStart"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@ -34,14 +40,16 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@drawable/circle"
|
||||||
|
android:backgroundTint="@color/element_background_light"
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:src="@drawable/ic_user_round"
|
android:src="@drawable/ic_user_fg"
|
||||||
app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace2"
|
app:layout_constraintBottom_toTopOf="@id/accountCreatedSpace2"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintHeight_percent="0.12"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace1"
|
app:layout_constraintTop_toBottomOf="@id/accountCreatedSpace1"
|
||||||
app:tint="@color/element_background_light" />
|
app:tint="?colorSecondary" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/accountCreatedSpace2"
|
android:id="@+id/accountCreatedSpace2"
|
||||||
|
@ -39,13 +39,15 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:src="@drawable/ic_user_round"
|
android:background="@drawable/circle"
|
||||||
|
android:backgroundTint="?colorSecondary"
|
||||||
|
android:src="@drawable/ic_user_fg"
|
||||||
app:layout_constraintBottom_toTopOf="@id/displayNameHeaderTitle"
|
app:layout_constraintBottom_toTopOf="@id/displayNameHeaderTitle"
|
||||||
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
|
||||||
app:layout_constraintHeight_percent="0.15"
|
app:layout_constraintHeight_percent="0.12"
|
||||||
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
|
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:tint="?colorSecondary" />
|
app:tint="@color/palette_white" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/displayNameHeaderTitle"
|
android:id="@+id/displayNameHeaderTitle"
|
||||||
|
@ -0,0 +1,115 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<nl.dionsegijn.konfetti.KonfettiView
|
||||||
|
android:id="@+id/viewKonfetti"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/ftueAuthGutterStart"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_start_percent" />
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.Guideline
|
||||||
|
android:id="@+id/ftueAuthGutterEnd"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintGuide_percent="@dimen/ftue_auth_gutter_end_percent" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/personalizationCompleteSpace1"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteLogo"
|
||||||
|
app:layout_constraintHeight_percent="0.10"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="spread_inside" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/personalizationCompleteLogo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@drawable/circle"
|
||||||
|
android:backgroundTint="?colorSecondary"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/ic_celebration"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace2"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHeight_percent="0.15"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace1"
|
||||||
|
app:tint="@color/palette_white" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/personalizationCompleteSpace2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteTitle"
|
||||||
|
app:layout_constraintHeight_percent="0.05"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteLogo" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/personalizationCompleteTitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Title.Medium"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ftue_personalize_complete_title"
|
||||||
|
android:textColor="?vctr_content_primary"
|
||||||
|
android:transitionName="loginTitleTransition"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSubtitle"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace2" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/personalizationCompleteSubtitle"
|
||||||
|
style="@style/Widget.Vector.TextView.Subtitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/ftue_personalize_complete_subtitle"
|
||||||
|
android:textColor="?vctr_content_secondary"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace4"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteTitle" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/personalizationCompleteSpace4"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteCta"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSubtitle" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/personalizationCompleteCta"
|
||||||
|
style="@style/Widget.Vector.Button.Login"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ftue_personalize_lets_go"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/personalizationCompleteSpace5"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/ftueAuthGutterEnd"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/ftueAuthGutterStart"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteSpace4" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:id="@+id/personalizationCompleteSpace5"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintHeight_percent="0.05"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/personalizationCompleteCta" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -31,6 +31,7 @@
|
|||||||
style="@style/Widget.Vector.Toolbar.Settings"
|
style="@style/Widget.Vector.Toolbar.Settings"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@android:color/transparent"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
app:layout_constraintBottom_toTopOf="@id/profilePictureView"
|
app:layout_constraintBottom_toTopOf="@id/profilePictureView"
|
||||||
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
|
app:layout_constraintTop_toBottomOf="@id/profilePictureToolbar"
|
||||||
@ -44,9 +45,9 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:adjustViewBounds="true"
|
android:adjustViewBounds="true"
|
||||||
|
android:background="@drawable/bg_rounded_button"
|
||||||
android:contentDescription="@null"
|
android:contentDescription="@null"
|
||||||
android:foreground="@drawable/bg_rounded_button"
|
android:src="@drawable/ic_user_fg"
|
||||||
android:src="@drawable/ic_user_round"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing"
|
app:layout_constraintBottom_toTopOf="@id/avatarTitleSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/profilePictureGutterEnd"
|
||||||
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height"
|
app:layout_constraintHeight_percent="@dimen/ftue_auth_profile_picture_height"
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
android:id="@+id/mapView"
|
android:id="@+id/mapView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:mapbox_renderTextureMode="true" />
|
app:mapbox_renderTextureMode="true"
|
||||||
|
app:showLocateButton="false" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -7,13 +7,34 @@
|
|||||||
|
|
||||||
<im.vector.app.features.location.MapTilerMapView
|
<im.vector.app.features.location.MapTilerMapView
|
||||||
android:id="@+id/mapView"
|
android:id="@+id/mapView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/shareLocationOptionsPicker"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:mapbox_renderTextureMode="true"
|
app:mapbox_renderTextureMode="true"
|
||||||
|
app:showLocateButton="true"
|
||||||
tools:background="#4F00" />
|
tools:background="#4F00" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/shareLocationPin"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@string/a11y_location_share_pin_on_map"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/shareLocationMapCenter"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/mapView"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/mapView" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/shareLocationMapCenter"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/mapView"
|
||||||
|
app:layout_constraintEnd_toEndOf="@id/mapView"
|
||||||
|
app:layout_constraintStart_toStartOf="@id/mapView"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/mapView" />
|
||||||
|
|
||||||
<im.vector.app.features.location.option.LocationSharingOptionPickerView
|
<im.vector.app.features.location.option.LocationSharingOptionPickerView
|
||||||
android:id="@+id/shareLocationOptionsPicker"
|
android:id="@+id/shareLocationOptionsPicker"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
<string name="ftue_profile_picture_title" translatable="false">Add a profile picture</string>
|
<string name="ftue_profile_picture_title" translatable="false">Add a profile picture</string>
|
||||||
<string name="ftue_profile_picture_subtitle" translatable="false">You can change this anytime.</string>
|
<string name="ftue_profile_picture_subtitle" translatable="false">You can change this anytime.</string>
|
||||||
|
<string name="ftue_personalize_lets_go" translatable="false">Let\'s go</string>
|
||||||
|
<string name="ftue_personalize_complete_title" translatable="false">You\'re all set!</string>
|
||||||
|
<string name="ftue_personalize_complete_subtitle" translatable="false">Your preferences have been saved.</string>
|
||||||
|
|
||||||
<string name="ftue_personalize_submit" translatable="false">Save and continue</string>
|
<string name="ftue_personalize_submit" translatable="false">Save and continue</string>
|
||||||
<string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string>
|
<string name="ftue_personalize_skip_this_step" translatable="false">Skip this step</string>
|
||||||
|
@ -2929,6 +2929,8 @@
|
|||||||
<string name="a11y_static_map_image">Map</string>
|
<string name="a11y_static_map_image">Map</string>
|
||||||
<!-- TODO delete -->
|
<!-- TODO delete -->
|
||||||
<string name="location_share" tools:ignore="UnusedResources">Share location</string>
|
<string name="location_share" tools:ignore="UnusedResources">Share location</string>
|
||||||
|
<string name="a11y_location_share_pin_on_map">Pin of selected location on map</string>
|
||||||
|
<string name="a11y_location_share_locate_button">Zoom to current location</string>
|
||||||
<string name="location_share_option_user_current">Share my current location</string>
|
<string name="location_share_option_user_current">Share my current location</string>
|
||||||
<string name="a11y_location_share_option_user_current_icon">Share my current location</string>
|
<string name="a11y_location_share_option_user_current_icon">Share my current location</string>
|
||||||
<string name="location_share_option_user_live">Share live location</string>
|
<string name="location_share_option_user_live">Share live location</string>
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user