Merge branch 'develop' into feature/ons/fix_live_location_sharing_permission
This commit is contained in:
commit
26a677edc2
@ -770,7 +770,7 @@ ij_kotlin_align_multiline_extends_list = false
|
|||||||
ij_kotlin_align_multiline_method_parentheses = false
|
ij_kotlin_align_multiline_method_parentheses = false
|
||||||
ij_kotlin_align_multiline_parameters = true
|
ij_kotlin_align_multiline_parameters = true
|
||||||
ij_kotlin_align_multiline_parameters_in_calls = false
|
ij_kotlin_align_multiline_parameters_in_calls = false
|
||||||
ij_kotlin_allow_trailing_comma = false
|
ij_kotlin_allow_trailing_comma = true
|
||||||
ij_kotlin_allow_trailing_comma_on_call_site = false
|
ij_kotlin_allow_trailing_comma_on_call_site = false
|
||||||
ij_kotlin_assignment_wrap = off
|
ij_kotlin_assignment_wrap = off
|
||||||
ij_kotlin_blank_lines_after_class_header = 0
|
ij_kotlin_blank_lines_after_class_header = 0
|
||||||
|
1
changelog.d/6200.bugfix
Normal file
1
changelog.d/6200.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Fixes room not being in space after upgrade
|
1
changelog.d/6437.feature
Normal file
1
changelog.d/6437.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Location sharing] - Delete action on a live message
|
1
changelog.d/6487.feature
Normal file
1
changelog.d/6487.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Timeline] - Collapse redacted events
|
1
changelog.d/6537.bugfix
Normal file
1
changelog.d/6537.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
[Location Share] - Wrong room live location status bar visibility in timeline
|
1
changelog.d/6546.feature
Normal file
1
changelog.d/6546.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Updates FTUE registration to include username availability check and update copy
|
1
changelog.d/6547.feature
Normal file
1
changelog.d/6547.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Updates the copy within the FTUE onboarding
|
1
changelog.d/6567.feature
Normal file
1
changelog.d/6567.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Share location with other apps
|
1
changelog.d/6579.bugfix
Normal file
1
changelog.d/6579.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Do not log the live location of the user
|
1
changelog.d/6584.misc
Normal file
1
changelog.d/6584.misc
Normal file
@ -0,0 +1 @@
|
|||||||
|
Adds NewAppLayoutEnabled feature flag
|
@ -13,7 +13,7 @@ ext.versions = [
|
|||||||
def gradle = "7.1.3"
|
def gradle = "7.1.3"
|
||||||
// Ref: https://kotlinlang.org/releases.html
|
// Ref: https://kotlinlang.org/releases.html
|
||||||
def kotlin = "1.6.21"
|
def kotlin = "1.6.21"
|
||||||
def kotlinCoroutines = "1.6.3"
|
def kotlinCoroutines = "1.6.4"
|
||||||
def dagger = "2.42"
|
def dagger = "2.42"
|
||||||
def retrofit = "2.9.0"
|
def retrofit = "2.9.0"
|
||||||
def arrow = "0.8.2"
|
def arrow = "0.8.2"
|
||||||
|
@ -156,6 +156,20 @@ object MatrixPatterns {
|
|||||||
return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
|
return matrixId?.substringAfter(":", missingDelimiterValue = "")?.takeIf { it.isNotEmpty() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract user name from a matrix id.
|
||||||
|
*
|
||||||
|
* @param matrixId
|
||||||
|
* @return null if the input is not a valid matrixId
|
||||||
|
*/
|
||||||
|
fun extractUserNameFromId(matrixId: String): String? {
|
||||||
|
return if (isUserId(matrixId)) {
|
||||||
|
matrixId.removePrefix("@").substringBefore(":", missingDelimiterValue = "")
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~),
|
* Orders which are not strings, or do not consist solely of ascii characters in the range \x20 (space) to \x7E (~),
|
||||||
* or consist of more than 50 characters, are forbidden and the field should be ignored if received.
|
* or consist of more than 50 characters, are forbidden and the field should be ignored if received.
|
||||||
|
@ -202,7 +202,7 @@ data class Event(
|
|||||||
* It will return a decrypted text message or an empty string otherwise.
|
* It will return a decrypted text message or an empty string otherwise.
|
||||||
*/
|
*/
|
||||||
fun getDecryptedTextSummary(): String? {
|
fun getDecryptedTextSummary(): String? {
|
||||||
if (isRedacted()) return "Message Deleted"
|
if (isRedacted()) return "Message removed"
|
||||||
val text = getDecryptedValue() ?: run {
|
val text = getDecryptedValue() ?: run {
|
||||||
if (isPoll()) {
|
if (isPoll()) {
|
||||||
return getPollQuestion() ?: "created a poll."
|
return getPollQuestion() ?: "created a poll."
|
||||||
@ -371,6 +371,8 @@ fun Event.isPoll(): Boolean = getClearType() in EventType.POLL_START || getClear
|
|||||||
|
|
||||||
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
fun Event.isSticker(): Boolean = getClearType() == EventType.STICKER
|
||||||
|
|
||||||
|
fun Event.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
|
||||||
|
|
||||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||||
return if (isEncrypted()) {
|
return if (isEncrypted()) {
|
||||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.api.session.room.location
|
package org.matrix.android.sdk.api.session.room.location
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||||
import org.matrix.android.sdk.api.util.Cancelable
|
import org.matrix.android.sdk.api.util.Cancelable
|
||||||
@ -59,16 +58,21 @@ interface LocationSharingService {
|
|||||||
*/
|
*/
|
||||||
suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
|
suspend fun stopLiveLocationShare(): UpdateLiveLocationShareResult
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redact (delete) the live associated to the given beacon info event id.
|
||||||
|
* @param beaconInfoEventId event id of the initial beacon info state event
|
||||||
|
* @param reason Optional reason string
|
||||||
|
*/
|
||||||
|
suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a LiveData on the list of current running live location shares.
|
* Returns a LiveData on the list of current running live location shares.
|
||||||
*/
|
*/
|
||||||
@MainThread
|
|
||||||
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
|
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a LiveData on the live location share summary with the given eventId.
|
* Returns a LiveData on the live location share summary with the given eventId.
|
||||||
* @param beaconInfoEventId event id of the initial beacon info state event
|
* @param beaconInfoEventId event id of the initial beacon info state event
|
||||||
*/
|
*/
|
||||||
@MainThread
|
|
||||||
fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
|
fun getLiveLocationShareSummary(beaconInfoEventId: String): LiveData<Optional<LiveLocationShareAggregatedSummary>>
|
||||||
}
|
}
|
||||||
|
@ -33,5 +33,7 @@ enum class VersioningState {
|
|||||||
/**
|
/**
|
||||||
* The room has been upgraded, and the new room has been joined.
|
* The room has been upgraded, and the new room has been joined.
|
||||||
*/
|
*/
|
||||||
UPGRADED_ROOM_JOINED,
|
UPGRADED_ROOM_JOINED;
|
||||||
|
|
||||||
|
fun isUpgraded() = this != NONE
|
||||||
}
|
}
|
||||||
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
|
|||||||
import org.matrix.android.sdk.api.session.events.model.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
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.isLiveLocation
|
||||||
import org.matrix.android.sdk.api.session.events.model.isPoll
|
import org.matrix.android.sdk.api.session.events.model.isPoll
|
||||||
import org.matrix.android.sdk.api.session.events.model.isReply
|
import org.matrix.android.sdk.api.session.events.model.isReply
|
||||||
import org.matrix.android.sdk.api.session.events.model.isSticker
|
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||||
@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean {
|
|||||||
return root.isSticker()
|
return root.isSticker()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun TimelineEvent.isLiveLocation(): Boolean {
|
||||||
|
return root.isLiveLocation()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns whether or not the event is a root thread event.
|
* Returns whether or not the event is a root thread event.
|
||||||
*/
|
*/
|
||||||
|
@ -604,14 +604,16 @@ internal class MXOlmDevice @Inject constructor(
|
|||||||
* @param sharedHistory MSC3061, this key is sharable on invite
|
* @param sharedHistory MSC3061, this key is sharable on invite
|
||||||
* @return true if the operation succeeds.
|
* @return true if the operation succeeds.
|
||||||
*/
|
*/
|
||||||
fun addInboundGroupSession(sessionId: String,
|
fun addInboundGroupSession(
|
||||||
|
sessionId: String,
|
||||||
sessionKey: String,
|
sessionKey: String,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
senderKey: String,
|
senderKey: String,
|
||||||
forwardingCurve25519KeyChain: List<String>,
|
forwardingCurve25519KeyChain: List<String>,
|
||||||
keysClaimed: Map<String, String>,
|
keysClaimed: Map<String, String>,
|
||||||
exportFormat: Boolean,
|
exportFormat: Boolean,
|
||||||
sharedHistory: Boolean): AddSessionResult {
|
sharedHistory: Boolean
|
||||||
|
): AddSessionResult {
|
||||||
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
||||||
if (exportFormat) {
|
if (exportFormat) {
|
||||||
OlmInboundGroupSession.importSession(sessionKey)
|
OlmInboundGroupSession.importSession(sessionKey)
|
||||||
|
@ -38,6 +38,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
|||||||
outgoingKeyRequestManager,
|
outgoingKeyRequestManager,
|
||||||
cryptoStore,
|
cryptoStore,
|
||||||
matrixConfiguration,
|
matrixConfiguration,
|
||||||
eventsManager)
|
eventsManager
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -250,8 +250,10 @@ internal class MXMegolmEncryption(
|
|||||||
* @param sessionInfo the session info
|
* @param sessionInfo the session info
|
||||||
* @param devicesByUser the devices map
|
* @param devicesByUser the devices map
|
||||||
*/
|
*/
|
||||||
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
|
private suspend fun shareUserDevicesKey(
|
||||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
sessionInfo: MXOutboundSessionInfo,
|
||||||
|
devicesByUser: Map<String, List<CryptoDeviceInfo>>
|
||||||
|
) {
|
||||||
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
|
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
|
||||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
|
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
|
||||||
}
|
}
|
||||||
|
@ -40,9 +40,9 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Schema version history:
|
* Schema version history:
|
||||||
* 0, 1, 2: legacy Riot-Android
|
* 0, 1, 2: legacy Riot-Android;
|
||||||
* 3: migrate to RiotX schema
|
* 3: migrate to RiotX schema;
|
||||||
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6).
|
||||||
*/
|
*/
|
||||||
internal class RealmCryptoStoreMigration @Inject constructor(
|
internal class RealmCryptoStoreMigration @Inject constructor(
|
||||||
private val clock: Clock,
|
private val clock: Clock,
|
||||||
|
@ -49,6 +49,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
|
|||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
|
||||||
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo032
|
||||||
|
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo033
|
||||||
import org.matrix.android.sdk.internal.util.Normalizer
|
import org.matrix.android.sdk.internal.util.Normalizer
|
||||||
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
private val normalizer: Normalizer
|
private val normalizer: Normalizer
|
||||||
) : MatrixRealmMigration(
|
) : MatrixRealmMigration(
|
||||||
dbName = "Session",
|
dbName = "Session",
|
||||||
schemaVersion = 32L,
|
schemaVersion = 33L,
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||||
@ -99,5 +100,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
|||||||
if (oldVersion < 30) MigrateSessionTo030(realm).perform()
|
if (oldVersion < 30) MigrateSessionTo030(realm).perform()
|
||||||
if (oldVersion < 31) MigrateSessionTo031(realm).perform()
|
if (oldVersion < 31) MigrateSessionTo031(realm).perform()
|
||||||
if (oldVersion < 32) MigrateSessionTo032(realm).perform()
|
if (oldVersion < 32) MigrateSessionTo032(realm).perform()
|
||||||
|
if (oldVersion < 33) MigrateSessionTo033(realm).perform()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* 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.database.migration
|
||||||
|
|
||||||
|
import io.realm.DynamicRealm
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.util.database.RealmMigrator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrating to:
|
||||||
|
* Live location sharing aggregated summary: adding new field relatedEventIds.
|
||||||
|
*/
|
||||||
|
internal class MigrateSessionTo033(realm: DynamicRealm) : RealmMigrator(realm, 33) {
|
||||||
|
|
||||||
|
override fun doMigrate(realm: DynamicRealm) {
|
||||||
|
realm.schema.get("LiveLocationShareAggregatedSummaryEntity")
|
||||||
|
?.addRealmListField(LiveLocationShareAggregatedSummaryEntityFields.RELATED_EVENT_IDS.`$`, String::class.java)
|
||||||
|
}
|
||||||
|
}
|
@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.matrix.android.sdk.internal.database.model.livelocation
|
package org.matrix.android.sdk.internal.database.model.livelocation
|
||||||
|
|
||||||
|
import io.realm.RealmList
|
||||||
import io.realm.RealmObject
|
import io.realm.RealmObject
|
||||||
import io.realm.annotations.PrimaryKey
|
import io.realm.annotations.PrimaryKey
|
||||||
|
|
||||||
@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
|
|||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
var eventId: String = "",
|
var eventId: String = "",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of event ids used to compute the aggregated summary data.
|
||||||
|
*/
|
||||||
|
var relatedEventIds: RealmList<String> = RealmList(),
|
||||||
|
|
||||||
var roomId: String = "",
|
var roomId: String = "",
|
||||||
|
|
||||||
var userId: String = "",
|
var userId: String = "",
|
||||||
|
@ -23,6 +23,11 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
|
|||||||
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
|
||||||
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
import org.matrix.android.sdk.internal.database.model.TimelineEventEntity
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
|
||||||
|
return realm.where<EventAnnotationsSummaryEntity>()
|
||||||
|
.equalTo(EventAnnotationsSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
|
internal fun EventAnnotationsSummaryEntity.Companion.where(realm: Realm, roomId: String, eventId: String): RealmQuery<EventAnnotationsSummaryEntity> {
|
||||||
return realm.where<EventAnnotationsSummaryEntity>()
|
return realm.where<EventAnnotationsSummaryEntity>()
|
||||||
.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
|
.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
|
||||||
@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r
|
|||||||
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
|
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
|
||||||
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
|
?: EventAnnotationsSummaryEntity.create(realm, roomId, eventId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun EventAnnotationsSummaryEntity.Companion.get(realm: Realm, eventId: String): EventAnnotationsSummaryEntity? {
|
||||||
|
return EventAnnotationsSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
}
|
||||||
|
@ -23,6 +23,14 @@ import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEnt
|
|||||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||||
|
|
||||||
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
|
||||||
|
realm: Realm,
|
||||||
|
eventId: String,
|
||||||
|
): RealmQuery<LiveLocationShareAggregatedSummaryEntity> {
|
||||||
|
return realm.where<LiveLocationShareAggregatedSummaryEntity>()
|
||||||
|
.equalTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
}
|
||||||
|
|
||||||
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.where(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
@ -72,6 +80,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
|
|||||||
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
|
return LiveLocationShareAggregatedSummaryEntity.where(realm, roomId, eventId).findFirst()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
|
||||||
|
realm: Realm,
|
||||||
|
eventId: String,
|
||||||
|
): LiveLocationShareAggregatedSummaryEntity? {
|
||||||
|
return LiveLocationShareAggregatedSummaryEntity.where(realm, eventId).findFirst()
|
||||||
|
}
|
||||||
|
|
||||||
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
|
internal fun LiveLocationShareAggregatedSummaryEntity.Companion.findActiveLiveInRoomForUser(
|
||||||
realm: Realm,
|
realm: Realm,
|
||||||
roomId: String,
|
roomId: String,
|
||||||
|
@ -88,6 +88,7 @@ import org.matrix.android.sdk.internal.session.room.EventRelationsAggregationPro
|
|||||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.DefaultPollAggregationProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
|
import org.matrix.android.sdk.internal.session.room.aggregation.poll.PollAggregationProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
import org.matrix.android.sdk.internal.session.room.create.RoomCreateEventProcessor
|
||||||
|
import org.matrix.android.sdk.internal.session.room.location.LiveLocationShareRedactionEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
import org.matrix.android.sdk.internal.session.room.prune.RedactionEventProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
|
||||||
@ -321,6 +322,10 @@ internal abstract class SessionModule {
|
|||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
|
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoSet
|
||||||
|
abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoSet
|
@IntoSet
|
||||||
abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor
|
abstract fun bindEventRelationsAggregationProcessor(processor: EventRelationsAggregationProcessor): EventInsertLiveProcessor
|
||||||
|
@ -58,11 +58,13 @@ import org.matrix.android.sdk.internal.session.room.directory.SetRoomDirectoryVi
|
|||||||
import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
|
import org.matrix.android.sdk.internal.session.room.location.CheckIfExistingActiveLiveTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultCheckIfExistingActiveLiveTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultGetActiveBeaconInfoForUserTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.location.DefaultRedactLiveLocationShareTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultSendLiveLocationTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultSendStaticLocationTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultStartLiveLocationShareTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
|
import org.matrix.android.sdk.internal.session.room.location.DefaultStopLiveLocationShareTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
|
import org.matrix.android.sdk.internal.session.room.location.GetActiveBeaconInfoForUserTask
|
||||||
|
import org.matrix.android.sdk.internal.session.room.location.RedactLiveLocationShareTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
|
import org.matrix.android.sdk.internal.session.room.location.SendLiveLocationTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
|
import org.matrix.android.sdk.internal.session.room.location.SendStaticLocationTask
|
||||||
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
|
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
|
||||||
@ -339,4 +341,7 @@ internal abstract class RoomModule {
|
|||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
|
abstract fun bindCheckIfExistingActiveLiveTask(task: DefaultCheckIfExistingActiveLiveTask): CheckIfExistingActiveLiveTask
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
abstract fun bindRedactLiveLocationShareTask(task: DefaultRedactLiveLocationShareTask): RedactLiveLocationShareTask
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
|
|||||||
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmList
|
||||||
import org.matrix.android.sdk.api.extensions.orTrue
|
import org.matrix.android.sdk.api.extensions.orTrue
|
||||||
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.events.model.toContent
|
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||||
@ -73,6 +74,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||||||
eventId = targetEventId
|
eventId = targetEventId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!isLive && !event.eventId.isNullOrEmpty()) {
|
||||||
|
// in this case, the received event is a new state event related to the previous one
|
||||||
|
addRelatedEventId(event.eventId, aggregatedSummary)
|
||||||
|
}
|
||||||
|
|
||||||
// remote event can stay with isLive == true while the local summary is no more active
|
// remote event can stay with isLive == true while the local summary is no more active
|
||||||
val isActive = aggregatedSummary.isActive.orTrue() && isLive
|
val isActive = aggregatedSummary.isActive.orTrue() && isLive
|
||||||
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
||||||
@ -144,6 +150,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||||||
roomId = roomId,
|
roomId = roomId,
|
||||||
eventId = relatedEventId
|
eventId = relatedEventId
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (!event.eventId.isNullOrEmpty()) {
|
||||||
|
addRelatedEventId(event.eventId, aggregatedSummary)
|
||||||
|
}
|
||||||
|
|
||||||
val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
|
val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
|
||||||
val currentLocationTimestamp = ContentMapper
|
val currentLocationTimestamp = ContentMapper
|
||||||
.map(aggregatedSummary.lastLocationContent)
|
.map(aggregatedSummary.lastLocationContent)
|
||||||
@ -160,6 +171,17 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun addRelatedEventId(
|
||||||
|
eventId: String,
|
||||||
|
aggregatedSummary: LiveLocationShareAggregatedSummaryEntity
|
||||||
|
) {
|
||||||
|
Timber.d("adding related event id $eventId to summary of id ${aggregatedSummary.eventId}")
|
||||||
|
val updatedEventIds = aggregatedSummary.relatedEventIds.toMutableList().also {
|
||||||
|
it.add(eventId)
|
||||||
|
}
|
||||||
|
aggregatedSummary.relatedEventIds = RealmList(*updatedEventIds.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) {
|
private fun deactivateAllPreviousBeacons(realm: Realm, roomId: String, userId: String, currentEventId: String) {
|
||||||
LiveLocationShareAggregatedSummaryEntity
|
LiveLocationShareAggregatedSummaryEntity
|
||||||
.findActiveLiveInRoomForUser(
|
.findActiveLiveInRoomForUser(
|
||||||
|
@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||||||
private val startLiveLocationShareTask: StartLiveLocationShareTask,
|
private val startLiveLocationShareTask: StartLiveLocationShareTask,
|
||||||
private val stopLiveLocationShareTask: StopLiveLocationShareTask,
|
private val stopLiveLocationShareTask: StopLiveLocationShareTask,
|
||||||
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
|
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
|
||||||
|
private val redactLiveLocationShareTask: RedactLiveLocationShareTask,
|
||||||
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
|
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
|
||||||
) : LocationSharingService {
|
) : LocationSharingService {
|
||||||
|
|
||||||
@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
|||||||
return stopLiveLocationShareTask.execute(params)
|
return stopLiveLocationShareTask.execute(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun redactLiveLocationShare(beaconInfoEventId: String, reason: String?) {
|
||||||
|
val params = RedactLiveLocationShareTask.Params(
|
||||||
|
roomId = roomId,
|
||||||
|
beaconInfoEventId = beaconInfoEventId,
|
||||||
|
reason = reason
|
||||||
|
)
|
||||||
|
return redactLiveLocationShareTask.execute(params)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
|
override fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>> {
|
||||||
return monarchy.findAllMappedWithChanges(
|
return monarchy.findAllMappedWithChanges(
|
||||||
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
|
{ LiveLocationShareAggregatedSummaryEntity.findRunningLiveInRoom(it, roomId = roomId) },
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 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.location
|
||||||
|
|
||||||
|
import io.realm.Realm
|
||||||
|
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.LocalEcho
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
|
import org.matrix.android.sdk.internal.database.query.where
|
||||||
|
import org.matrix.android.sdk.internal.session.EventInsertLiveProcessor
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens to the database for the insertion of any redaction event.
|
||||||
|
* Delete specifically the aggregated summary related to a redacted live location share event.
|
||||||
|
*/
|
||||||
|
internal class LiveLocationShareRedactionEventProcessor @Inject constructor() : EventInsertLiveProcessor {
|
||||||
|
|
||||||
|
override fun shouldProcess(eventId: String, eventType: String, insertType: EventInsertType): Boolean {
|
||||||
|
return eventType == EventType.REDACTION && insertType != EventInsertType.LOCAL_ECHO
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun process(realm: Realm, event: Event) {
|
||||||
|
if (event.redacts.isNullOrBlank() || LocalEcho.isLocalEchoId(event.eventId.orEmpty())) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val redactedEvent = EventEntity.where(realm, eventId = event.redacts).findFirst()
|
||||||
|
?: return
|
||||||
|
|
||||||
|
if (redactedEvent.type in EventType.STATE_ROOM_BEACON_INFO) {
|
||||||
|
val liveSummary = LiveLocationShareAggregatedSummaryEntity.get(realm, eventId = redactedEvent.eventId)
|
||||||
|
|
||||||
|
if (liveSummary != null) {
|
||||||
|
Timber.d("deleting live summary with id: ${liveSummary.eventId}")
|
||||||
|
liveSummary.deleteFromRealm()
|
||||||
|
val annotationsSummary = EventAnnotationsSummaryEntity.get(realm, eventId = redactedEvent.eventId)
|
||||||
|
if (annotationsSummary != null) {
|
||||||
|
Timber.d("deleting annotation summary with id: ${annotationsSummary.eventId}")
|
||||||
|
annotationsSummary.deleteFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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.location
|
||||||
|
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import org.matrix.android.sdk.internal.database.awaitTransaction
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.query.get
|
||||||
|
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.LocalEchoEventFactory
|
||||||
|
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessor
|
||||||
|
import org.matrix.android.sdk.internal.task.Task
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
internal interface RedactLiveLocationShareTask : Task<RedactLiveLocationShareTask.Params, Unit> {
|
||||||
|
data class Params(
|
||||||
|
val roomId: String,
|
||||||
|
val beaconInfoEventId: String,
|
||||||
|
val reason: String?
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DefaultRedactLiveLocationShareTask @Inject constructor(
|
||||||
|
@SessionDatabase private val realmConfiguration: RealmConfiguration,
|
||||||
|
private val localEchoEventFactory: LocalEchoEventFactory,
|
||||||
|
private val eventSenderProcessor: EventSenderProcessor,
|
||||||
|
) : RedactLiveLocationShareTask {
|
||||||
|
|
||||||
|
override suspend fun execute(params: RedactLiveLocationShareTask.Params) {
|
||||||
|
val relatedEventIds = getRelatedEventIdsOfLive(params.beaconInfoEventId)
|
||||||
|
Timber.d("beacon with id ${params.beaconInfoEventId} has related event ids: ${relatedEventIds.joinToString(", ")}")
|
||||||
|
|
||||||
|
postRedactionWithLocalEcho(
|
||||||
|
eventId = params.beaconInfoEventId,
|
||||||
|
roomId = params.roomId,
|
||||||
|
reason = params.reason
|
||||||
|
)
|
||||||
|
relatedEventIds.forEach { eventId ->
|
||||||
|
postRedactionWithLocalEcho(
|
||||||
|
eventId = eventId,
|
||||||
|
roomId = params.roomId,
|
||||||
|
reason = params.reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getRelatedEventIdsOfLive(beaconInfoEventId: String): List<String> {
|
||||||
|
return awaitTransaction(realmConfiguration) { realm ->
|
||||||
|
val aggregatedSummaryEntity = LiveLocationShareAggregatedSummaryEntity.get(
|
||||||
|
realm = realm,
|
||||||
|
eventId = beaconInfoEventId
|
||||||
|
)
|
||||||
|
aggregatedSummaryEntity?.relatedEventIds?.toList() ?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postRedactionWithLocalEcho(eventId: String, roomId: String, reason: String?) {
|
||||||
|
Timber.d("posting redaction for event of id $eventId")
|
||||||
|
val redactionEcho = localEchoEventFactory.createRedactEvent(roomId, eventId, reason)
|
||||||
|
localEchoEventFactory.createLocalEcho(redactionEcho)
|
||||||
|
eventSenderProcessor.postRedaction(redactionEcho, reason)
|
||||||
|
}
|
||||||
|
}
|
@ -74,6 +74,8 @@ internal class RedactionEventProcessor @Inject constructor() : EventInsertLivePr
|
|||||||
when (typeToPrune) {
|
when (typeToPrune) {
|
||||||
EventType.ENCRYPTED,
|
EventType.ENCRYPTED,
|
||||||
EventType.MESSAGE,
|
EventType.MESSAGE,
|
||||||
|
in EventType.STATE_ROOM_BEACON_INFO,
|
||||||
|
in EventType.BEACON_LOCATION_DATA,
|
||||||
in EventType.POLL_START -> {
|
in EventType.POLL_START -> {
|
||||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||||
|
@ -35,6 +35,23 @@ class MatrixPatternsTest {
|
|||||||
MatrixPatterns.isUserId(input) shouldBeEqualTo expected
|
MatrixPatterns.isUserId(input) shouldBeEqualTo expected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given matrix id cases, when extracting userName, then returns expected`() {
|
||||||
|
val cases = listOf(
|
||||||
|
MatrixIdCase("foobar", userName = null),
|
||||||
|
MatrixIdCase("@foobar", userName = null),
|
||||||
|
MatrixIdCase("foobar@matrix.org", userName = null),
|
||||||
|
MatrixIdCase("@foobar: matrix.org", userName = null),
|
||||||
|
MatrixIdCase("foobar:matrix.org", userName = null),
|
||||||
|
MatrixIdCase("@foobar:matrix.org", userName = "foobar"),
|
||||||
|
)
|
||||||
|
|
||||||
|
cases.forEach { (input, expected) ->
|
||||||
|
MatrixPatterns.extractUserNameFromId(input) shouldBeEqualTo expected
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class UserIdCase(val input: String, val isUserId: Boolean)
|
private data class UserIdCase(val input: String, val isUserId: Boolean)
|
||||||
|
private data class MatrixIdCase(val input: String, val userName: String?)
|
||||||
|
@ -18,6 +18,7 @@ package org.matrix.android.sdk.internal.session.room.aggregation.livelocation
|
|||||||
|
|
||||||
import androidx.work.ExistingWorkPolicy
|
import androidx.work.ExistingWorkPolicy
|
||||||
import org.amshove.kluent.shouldBeEqualTo
|
import org.amshove.kluent.shouldBeEqualTo
|
||||||
|
import org.amshove.kluent.shouldContain
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
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.events.model.UnsignedData
|
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||||
@ -199,9 +200,10 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
age = 123,
|
age = 123,
|
||||||
replacesState = AN_EVENT_ID
|
replacesState = AN_EVENT_ID
|
||||||
)
|
)
|
||||||
|
val stateEventId = "state-event-id"
|
||||||
val event = Event(
|
val event = Event(
|
||||||
senderId = A_SENDER_ID,
|
senderId = A_SENDER_ID,
|
||||||
eventId = "",
|
eventId = stateEventId,
|
||||||
unsignedData = unsignedData
|
unsignedData = unsignedData
|
||||||
)
|
)
|
||||||
val beaconInfo = MessageBeaconInfoContent(
|
val beaconInfo = MessageBeaconInfoContent(
|
||||||
@ -237,6 +239,7 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
|
aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
|
||||||
aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
|
aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
|
||||||
aggregatedEntity.isActive shouldBeEqualTo false
|
aggregatedEntity.isActive shouldBeEqualTo false
|
||||||
|
aggregatedEntity.relatedEventIds shouldContain stateEventId
|
||||||
aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
|
aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
|
||||||
aggregatedEntity.lastLocationContent shouldBeEqualTo null
|
aggregatedEntity.lastLocationContent shouldBeEqualTo null
|
||||||
previousEntities.forEach { entity ->
|
previousEntities.forEach { entity ->
|
||||||
@ -324,7 +327,7 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
||||||
unstableTimestampMillis = A_TIMESTAMP
|
unstableTimestampMillis = A_TIMESTAMP
|
||||||
)
|
)
|
||||||
givenLastSummaryQueryReturns(
|
val aggregatedEntity = givenLastSummaryQueryReturns(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
beaconLocationContent = lastBeaconLocationContent
|
beaconLocationContent = lastBeaconLocationContent
|
||||||
@ -340,6 +343,7 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
result shouldBeEqualTo false
|
result shouldBeEqualTo false
|
||||||
|
aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@ -353,7 +357,7 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
||||||
unstableTimestampMillis = A_TIMESTAMP - 60_000
|
unstableTimestampMillis = A_TIMESTAMP - 60_000
|
||||||
)
|
)
|
||||||
val entity = givenLastSummaryQueryReturns(
|
val aggregatedEntity = givenLastSummaryQueryReturns(
|
||||||
eventId = AN_EVENT_ID,
|
eventId = AN_EVENT_ID,
|
||||||
roomId = A_ROOM_ID,
|
roomId = A_ROOM_ID,
|
||||||
beaconLocationContent = lastBeaconLocationContent
|
beaconLocationContent = lastBeaconLocationContent
|
||||||
@ -369,7 +373,8 @@ internal class LiveLocationAggregationProcessorTest {
|
|||||||
)
|
)
|
||||||
|
|
||||||
result shouldBeEqualTo true
|
result shouldBeEqualTo true
|
||||||
val savedLocationData = ContentMapper.map(entity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
|
aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
|
||||||
|
val savedLocationData = ContentMapper.map(aggregatedEntity.lastLocationContent).toModel<MessageBeaconLocationDataContent>()
|
||||||
savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
|
savedLocationData?.getBestTimestampMillis() shouldBeEqualTo A_TIMESTAMP
|
||||||
savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
|
savedLocationData?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
|
||||||
}
|
}
|
||||||
|
@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations
|
|||||||
import io.mockk.coEvery
|
import io.mockk.coEvery
|
||||||
import io.mockk.coVerify
|
import io.mockk.coVerify
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.mockkStatic
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.runs
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.unmockkAll
|
import io.mockk.unmockkAll
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@ -52,6 +54,7 @@ private const val A_LONGITUDE = 40.0
|
|||||||
private const val AN_UNCERTAINTY = 5.0
|
private const val AN_UNCERTAINTY = 5.0
|
||||||
private const val A_TIMEOUT = 15_000L
|
private const val A_TIMEOUT = 15_000L
|
||||||
private const val A_DESCRIPTION = "description"
|
private const val A_DESCRIPTION = "description"
|
||||||
|
private const val A_REASON = "reason"
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
internal class DefaultLocationSharingServiceTest {
|
internal class DefaultLocationSharingServiceTest {
|
||||||
@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest {
|
|||||||
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
|
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
|
||||||
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
|
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
|
||||||
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
|
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
|
||||||
|
private val redactLiveLocationShareTask = mockk<RedactLiveLocationShareTask>()
|
||||||
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
|
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
|
||||||
|
|
||||||
private val defaultLocationSharingService = DefaultLocationSharingService(
|
private val defaultLocationSharingService = DefaultLocationSharingService(
|
||||||
@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest {
|
|||||||
startLiveLocationShareTask = startLiveLocationShareTask,
|
startLiveLocationShareTask = startLiveLocationShareTask,
|
||||||
stopLiveLocationShareTask = stopLiveLocationShareTask,
|
stopLiveLocationShareTask = stopLiveLocationShareTask,
|
||||||
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
|
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
|
||||||
|
redactLiveLocationShareTask = redactLiveLocationShareTask,
|
||||||
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
|
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest {
|
|||||||
coVerify { stopLiveLocationShareTask.execute(expectedParams) }
|
coVerify { stopLiveLocationShareTask.execute(expectedParams) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `live location share can be redacted`() = runTest {
|
||||||
|
coEvery { redactLiveLocationShareTask.execute(any()) } just runs
|
||||||
|
|
||||||
|
defaultLocationSharingService.redactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON)
|
||||||
|
|
||||||
|
val expectedParams = RedactLiveLocationShareTask.Params(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
beaconInfoEventId = AN_EVENT_ID,
|
||||||
|
reason = A_REASON
|
||||||
|
)
|
||||||
|
coVerify { redactLiveLocationShareTask.execute(expectedParams) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `livedata of live summaries is correctly computed`() {
|
fun `livedata of live summaries is correctly computed`() {
|
||||||
val entity = LiveLocationShareAggregatedSummaryEntity()
|
val entity = LiveLocationShareAggregatedSummaryEntity()
|
||||||
|
@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* 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.location
|
||||||
|
|
||||||
|
import io.mockk.unmockkAll
|
||||||
|
import io.realm.RealmList
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeEventSenderProcessor
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeLocalEchoEventFactory
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeRealm
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeRealmConfiguration
|
||||||
|
import org.matrix.android.sdk.test.fakes.givenEqualTo
|
||||||
|
import org.matrix.android.sdk.test.fakes.givenFindFirst
|
||||||
|
|
||||||
|
private const val A_ROOM_ID = "room-id"
|
||||||
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
private const val AN_EVENT_ID_1 = "event-id-1"
|
||||||
|
private const val AN_EVENT_ID_2 = "event-id-2"
|
||||||
|
private const val AN_EVENT_ID_3 = "event-id-3"
|
||||||
|
private const val A_REASON = "reason"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class DefaultRedactLiveLocationShareTaskTest {
|
||||||
|
|
||||||
|
private val fakeRealmConfiguration = FakeRealmConfiguration()
|
||||||
|
private val fakeLocalEchoEventFactory = FakeLocalEchoEventFactory()
|
||||||
|
private val fakeEventSenderProcessor = FakeEventSenderProcessor()
|
||||||
|
private val fakeRealm = FakeRealm()
|
||||||
|
|
||||||
|
private val defaultRedactLiveLocationShareTask = DefaultRedactLiveLocationShareTask(
|
||||||
|
realmConfiguration = fakeRealmConfiguration.instance,
|
||||||
|
localEchoEventFactory = fakeLocalEchoEventFactory.instance,
|
||||||
|
eventSenderProcessor = fakeEventSenderProcessor
|
||||||
|
)
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun tearDown() {
|
||||||
|
unmockkAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given parameters when redacting then post redact events and related and creates redact local echos`() = runTest {
|
||||||
|
val params = createParams()
|
||||||
|
val relatedEventIds = listOf(AN_EVENT_ID_1, AN_EVENT_ID_2, AN_EVENT_ID_3)
|
||||||
|
val aggregatedSummaryEntity = createSummary(relatedEventIds)
|
||||||
|
givenSummaryForId(AN_EVENT_ID, aggregatedSummaryEntity)
|
||||||
|
fakeRealmConfiguration.givenAwaitTransaction<List<String>>(fakeRealm.instance)
|
||||||
|
val redactEvents = givenCreateRedactEventWithLocalEcho(relatedEventIds + AN_EVENT_ID)
|
||||||
|
givenPostRedaction(redactEvents)
|
||||||
|
|
||||||
|
defaultRedactLiveLocationShareTask.execute(params)
|
||||||
|
|
||||||
|
verifyCreateRedactEventForEventIds(relatedEventIds + AN_EVENT_ID)
|
||||||
|
verifyCreateLocalEchoForEvents(redactEvents)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createParams() = RedactLiveLocationShareTask.Params(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
beaconInfoEventId = AN_EVENT_ID,
|
||||||
|
reason = A_REASON
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createSummary(relatedEventIds: List<String>): LiveLocationShareAggregatedSummaryEntity {
|
||||||
|
return LiveLocationShareAggregatedSummaryEntity(
|
||||||
|
eventId = AN_EVENT_ID,
|
||||||
|
relatedEventIds = RealmList(*relatedEventIds.toTypedArray()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenSummaryForId(eventId: String, aggregatedSummaryEntity: LiveLocationShareAggregatedSummaryEntity) {
|
||||||
|
fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
|
||||||
|
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, eventId)
|
||||||
|
.givenFindFirst(aggregatedSummaryEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenCreateRedactEventWithLocalEcho(eventIds: List<String>): List<Event> {
|
||||||
|
return eventIds.map { eventId ->
|
||||||
|
fakeLocalEchoEventFactory.givenCreateRedactEvent(
|
||||||
|
eventId = eventId,
|
||||||
|
withLocalEcho = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenPostRedaction(redactEvents: List<Event>) {
|
||||||
|
redactEvents.forEach {
|
||||||
|
fakeEventSenderProcessor.givenPostRedaction(event = it, reason = A_REASON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCreateRedactEventForEventIds(eventIds: List<String>) {
|
||||||
|
eventIds.forEach { eventId ->
|
||||||
|
fakeLocalEchoEventFactory.verifyCreateRedactEvent(
|
||||||
|
roomId = A_ROOM_ID,
|
||||||
|
eventId = eventId,
|
||||||
|
reason = A_REASON
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun verifyCreateLocalEchoForEvents(events: List<Event>) {
|
||||||
|
events.forEach { redactionEvent ->
|
||||||
|
fakeLocalEchoEventFactory.verifyCreateLocalEcho(redactionEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
* 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.location
|
||||||
|
|
||||||
|
import io.mockk.every
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.verify
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.junit.Test
|
||||||
|
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.internal.database.model.EventAnnotationsSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventAnnotationsSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventEntityFields
|
||||||
|
import org.matrix.android.sdk.internal.database.model.EventInsertType
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntity
|
||||||
|
import org.matrix.android.sdk.internal.database.model.livelocation.LiveLocationShareAggregatedSummaryEntityFields
|
||||||
|
import org.matrix.android.sdk.test.fakes.FakeRealm
|
||||||
|
import org.matrix.android.sdk.test.fakes.givenDelete
|
||||||
|
import org.matrix.android.sdk.test.fakes.givenEqualTo
|
||||||
|
import org.matrix.android.sdk.test.fakes.givenFindFirst
|
||||||
|
|
||||||
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
private const val A_REDACTED_EVENT_ID = "redacted-event-id"
|
||||||
|
|
||||||
|
@ExperimentalCoroutinesApi
|
||||||
|
class LiveLocationShareRedactionEventProcessorTest {
|
||||||
|
|
||||||
|
private val liveLocationShareRedactionEventProcessor = LiveLocationShareRedactionEventProcessor()
|
||||||
|
private val fakeRealm = FakeRealm()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event when checking if it should be processed then only event of type REDACTED is processed`() {
|
||||||
|
val eventId = AN_EVENT_ID
|
||||||
|
val eventType = EventType.REDACTION
|
||||||
|
val insertType = EventInsertType.INCREMENTAL_SYNC
|
||||||
|
|
||||||
|
val result = liveLocationShareRedactionEventProcessor.shouldProcess(
|
||||||
|
eventId = eventId,
|
||||||
|
eventType = eventType,
|
||||||
|
insertType = insertType
|
||||||
|
)
|
||||||
|
|
||||||
|
result shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event when checking if it should be processed then local echo is not processed`() {
|
||||||
|
val eventId = AN_EVENT_ID
|
||||||
|
val eventType = EventType.REDACTION
|
||||||
|
val insertType = EventInsertType.LOCAL_ECHO
|
||||||
|
|
||||||
|
val result = liveLocationShareRedactionEventProcessor.shouldProcess(
|
||||||
|
eventId = eventId,
|
||||||
|
eventType = eventType,
|
||||||
|
insertType = insertType
|
||||||
|
)
|
||||||
|
|
||||||
|
result shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given a redacted live location share event when processing it then related summaries are deleted from database`() = runTest {
|
||||||
|
val event = Event(eventId = AN_EVENT_ID, redacts = A_REDACTED_EVENT_ID)
|
||||||
|
val redactedEventEntity = EventEntity(eventId = A_REDACTED_EVENT_ID, type = EventType.STATE_ROOM_BEACON_INFO.first())
|
||||||
|
fakeRealm.givenWhere<EventEntity>()
|
||||||
|
.givenEqualTo(EventEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
|
||||||
|
.givenFindFirst(redactedEventEntity)
|
||||||
|
val liveSummary = mockk<LiveLocationShareAggregatedSummaryEntity>()
|
||||||
|
every { liveSummary.eventId } returns A_REDACTED_EVENT_ID
|
||||||
|
liveSummary.givenDelete()
|
||||||
|
fakeRealm.givenWhere<LiveLocationShareAggregatedSummaryEntity>()
|
||||||
|
.givenEqualTo(LiveLocationShareAggregatedSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
|
||||||
|
.givenFindFirst(liveSummary)
|
||||||
|
val annotationsSummary = mockk<EventAnnotationsSummaryEntity>()
|
||||||
|
every { annotationsSummary.eventId } returns A_REDACTED_EVENT_ID
|
||||||
|
annotationsSummary.givenDelete()
|
||||||
|
fakeRealm.givenWhere<EventAnnotationsSummaryEntity>()
|
||||||
|
.givenEqualTo(EventAnnotationsSummaryEntityFields.EVENT_ID, A_REDACTED_EVENT_ID)
|
||||||
|
.givenFindFirst(annotationsSummary)
|
||||||
|
|
||||||
|
liveLocationShareRedactionEventProcessor.process(fakeRealm.instance, event = event)
|
||||||
|
|
||||||
|
verify {
|
||||||
|
liveSummary.deleteFromRealm()
|
||||||
|
annotationsSummary.deleteFromRealm()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -27,4 +27,8 @@ internal class FakeEventSenderProcessor : EventSenderProcessor by mockk() {
|
|||||||
fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
|
fun givenPostEventReturns(event: Event, cancelable: Cancelable) {
|
||||||
every { postEvent(event) } returns cancelable
|
every { postEvent(event) } returns cancelable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenPostRedaction(event: Event, reason: String?) {
|
||||||
|
every { postRedaction(event, reason) } returns mockk()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,24 +46,6 @@ internal class FakeLocalEchoEventFactory {
|
|||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
|
|
||||||
val event = Event()
|
|
||||||
every {
|
|
||||||
instance.createLiveLocationEvent(
|
|
||||||
beaconInfoEventId = any(),
|
|
||||||
roomId = any(),
|
|
||||||
latitude = any(),
|
|
||||||
longitude = any(),
|
|
||||||
uncertainty = any()
|
|
||||||
)
|
|
||||||
} returns event
|
|
||||||
|
|
||||||
if (withLocalEcho) {
|
|
||||||
every { instance.createLocalEcho(event) } just runs
|
|
||||||
}
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
fun verifyCreateStaticLocationEvent(
|
fun verifyCreateStaticLocationEvent(
|
||||||
roomId: String,
|
roomId: String,
|
||||||
latitude: Double,
|
latitude: Double,
|
||||||
@ -82,6 +64,24 @@ internal class FakeLocalEchoEventFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCreateLiveLocationEvent(withLocalEcho: Boolean): Event {
|
||||||
|
val event = Event()
|
||||||
|
every {
|
||||||
|
instance.createLiveLocationEvent(
|
||||||
|
beaconInfoEventId = any(),
|
||||||
|
roomId = any(),
|
||||||
|
latitude = any(),
|
||||||
|
longitude = any(),
|
||||||
|
uncertainty = any()
|
||||||
|
)
|
||||||
|
} returns event
|
||||||
|
|
||||||
|
if (withLocalEcho) {
|
||||||
|
every { instance.createLocalEcho(event) } just runs
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
fun verifyCreateLiveLocationEvent(
|
fun verifyCreateLiveLocationEvent(
|
||||||
roomId: String,
|
roomId: String,
|
||||||
beaconInfoEventId: String,
|
beaconInfoEventId: String,
|
||||||
@ -100,6 +100,36 @@ internal class FakeLocalEchoEventFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun givenCreateRedactEvent(eventId: String, withLocalEcho: Boolean): Event {
|
||||||
|
val event = Event()
|
||||||
|
every {
|
||||||
|
instance.createRedactEvent(
|
||||||
|
roomId = any(),
|
||||||
|
eventId = eventId,
|
||||||
|
reason = any()
|
||||||
|
)
|
||||||
|
} returns event
|
||||||
|
|
||||||
|
if (withLocalEcho) {
|
||||||
|
every { instance.createLocalEcho(event) } just runs
|
||||||
|
}
|
||||||
|
return event
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyCreateRedactEvent(
|
||||||
|
roomId: String,
|
||||||
|
eventId: String,
|
||||||
|
reason: String?
|
||||||
|
) {
|
||||||
|
verify {
|
||||||
|
instance.createRedactEvent(
|
||||||
|
roomId = roomId,
|
||||||
|
eventId = eventId,
|
||||||
|
reason = reason
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun verifyCreateLocalEcho(event: Event) {
|
fun verifyCreateLocalEcho(event: Event) {
|
||||||
verify { instance.createLocalEcho(event) }
|
verify { instance.createLocalEcho(event) }
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes
|
|||||||
|
|
||||||
import io.mockk.MockKVerificationScope
|
import io.mockk.MockKVerificationScope
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
|
import io.mockk.runs
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import io.realm.Realm
|
import io.realm.Realm
|
||||||
import io.realm.RealmModel
|
import io.realm.RealmModel
|
||||||
|
import io.realm.RealmObject
|
||||||
import io.realm.RealmQuery
|
import io.realm.RealmQuery
|
||||||
import io.realm.RealmResults
|
import io.realm.RealmResults
|
||||||
import io.realm.kotlin.where
|
import io.realm.kotlin.where
|
||||||
@ -97,3 +100,10 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
|
|||||||
every { isNotNull(fieldName) } returns this
|
every { isNotNull(fieldName) } returns this
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should be called on a mocked RealmObject and not on a real RealmObject so that the underlying final method is mocked.
|
||||||
|
*/
|
||||||
|
fun RealmObject.givenDelete() {
|
||||||
|
every { deleteFromRealm() } just runs
|
||||||
|
}
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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.test.fakes
|
||||||
|
|
||||||
|
import io.mockk.coEvery
|
||||||
|
import io.mockk.mockk
|
||||||
|
import io.mockk.mockkStatic
|
||||||
|
import io.mockk.slot
|
||||||
|
import io.realm.Realm
|
||||||
|
import io.realm.RealmConfiguration
|
||||||
|
import org.matrix.android.sdk.internal.database.awaitTransaction
|
||||||
|
|
||||||
|
internal class FakeRealmConfiguration {
|
||||||
|
|
||||||
|
init {
|
||||||
|
mockkStatic("org.matrix.android.sdk.internal.database.AsyncTransactionKt")
|
||||||
|
}
|
||||||
|
|
||||||
|
val instance = mockk<RealmConfiguration>()
|
||||||
|
|
||||||
|
fun <T> givenAwaitTransaction(realm: Realm) {
|
||||||
|
val transaction = slot<suspend (Realm) -> T>()
|
||||||
|
coEvery { awaitTransaction(instance, capture(transaction)) } coAnswers {
|
||||||
|
secondArg<suspend (Realm) -> T>().invoke(realm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -280,10 +280,10 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
nightly {
|
nightly {
|
||||||
|
initWith release
|
||||||
applicationIdSuffix ".nightly"
|
applicationIdSuffix ".nightly"
|
||||||
versionNameSuffix "-nightly"
|
versionNameSuffix "-nightly"
|
||||||
|
|
||||||
initWith release
|
|
||||||
// Just override the background color of the launcher icon for the nightly build.
|
// Just override the background color of the launcher icon for the nightly build.
|
||||||
resValue "color", "launcher_background", "#07007E"
|
resValue "color", "launcher_background", "#07007E"
|
||||||
|
|
||||||
|
@ -80,6 +80,11 @@ class DebugFeaturesStateFactory @Inject constructor(
|
|||||||
key = DebugFeatureKeys.startDmOnFirstMsg,
|
key = DebugFeatureKeys.startDmOnFirstMsg,
|
||||||
factory = VectorFeatures::shouldStartDmOnFirstMessage
|
factory = VectorFeatures::shouldStartDmOnFirstMessage
|
||||||
),
|
),
|
||||||
|
createBooleanFeature(
|
||||||
|
label = "Enable New App Layout",
|
||||||
|
key = DebugFeatureKeys.newAppLayoutEnabled,
|
||||||
|
factory = VectorFeatures::isNewAppLayoutEnabled
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,9 @@ class DebugVectorFeatures(
|
|||||||
override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg)
|
override fun shouldStartDmOnFirstMessage(): Boolean = read(DebugFeatureKeys.startDmOnFirstMsg)
|
||||||
?: vectorFeatures.shouldStartDmOnFirstMessage()
|
?: vectorFeatures.shouldStartDmOnFirstMessage()
|
||||||
|
|
||||||
|
override fun isNewAppLayoutEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled)
|
||||||
|
?: vectorFeatures.isNewAppLayoutEnabled()
|
||||||
|
|
||||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
it.remove(key)
|
it.remove(key)
|
||||||
@ -131,4 +134,5 @@ object DebugFeatureKeys {
|
|||||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||||
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
|
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
|
||||||
val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg")
|
val startDmOnFirstMsg = booleanPreferencesKey("start-dm-on-first-msg")
|
||||||
|
val newAppLayoutEnabled = booleanPreferencesKey("new-app-layout-enabled")
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ package im.vector.app.core.extensions
|
|||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import com.google.i18n.phonenumbers.NumberParseException
|
import com.google.i18n.phonenumbers.NumberParseException
|
||||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||||
|
import org.matrix.android.sdk.api.MatrixPatterns
|
||||||
import org.matrix.android.sdk.api.extensions.ensurePrefix
|
import org.matrix.android.sdk.api.extensions.ensurePrefix
|
||||||
|
|
||||||
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
fun Boolean.toOnOff() = if (this) "ON" else "OFF"
|
||||||
@ -30,6 +31,8 @@ inline fun <T> T.ooi(block: (T) -> Unit): T = also(block)
|
|||||||
*/
|
*/
|
||||||
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
fun CharSequence.isEmail() = Patterns.EMAIL_ADDRESS.matcher(this).matches()
|
||||||
|
|
||||||
|
fun CharSequence.isMatrixId() = MatrixPatterns.isUserId(this.toString())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return empty CharSequence if the CharSequence is null.
|
* Return empty CharSequence if the CharSequence is null.
|
||||||
*/
|
*/
|
||||||
|
@ -44,8 +44,15 @@ fun TextInputLayout.content() = editText().text.toString()
|
|||||||
fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty()
|
fun TextInputLayout.hasContent() = !editText().text.isNullOrEmpty()
|
||||||
|
|
||||||
fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) {
|
fun TextInputLayout.clearErrorOnChange(lifecycleOwner: LifecycleOwner) {
|
||||||
|
onTextChange(lifecycleOwner) {
|
||||||
|
error = null
|
||||||
|
isErrorEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun TextInputLayout.onTextChange(lifecycleOwner: LifecycleOwner, action: (CharSequence) -> Unit) {
|
||||||
editText().textChanges()
|
editText().textChanges()
|
||||||
.onEach { error = null }
|
.onEach(action)
|
||||||
.launchIn(lifecycleOwner.lifecycleScope)
|
.launchIn(lifecycleOwner.lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,6 @@ class AndroidSystemSettingsProvider @Inject constructor(
|
|||||||
) : SystemSettingsProvider {
|
) : SystemSettingsProvider {
|
||||||
|
|
||||||
override fun getSystemFontScale(): Float {
|
override fun getSystemFontScale(): Float {
|
||||||
return Settings.System.getFloat(context.contentResolver, Settings.System.FONT_SCALE)
|
return Settings.System.getFloat(context.contentResolver, Settings.System.FONT_SCALE, 1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ interface VectorFeatures {
|
|||||||
fun isScreenSharingEnabled(): Boolean
|
fun isScreenSharingEnabled(): Boolean
|
||||||
fun forceUsageOfOpusEncoder(): Boolean
|
fun forceUsageOfOpusEncoder(): Boolean
|
||||||
fun shouldStartDmOnFirstMessage(): Boolean
|
fun shouldStartDmOnFirstMessage(): Boolean
|
||||||
|
fun isNewAppLayoutEnabled(): Boolean
|
||||||
|
|
||||||
enum class OnboardingVariant {
|
enum class OnboardingVariant {
|
||||||
LEGACY,
|
LEGACY,
|
||||||
@ -52,4 +53,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||||||
override fun isScreenSharingEnabled(): Boolean = true
|
override fun isScreenSharingEnabled(): Boolean = true
|
||||||
override fun forceUsageOfOpusEncoder(): Boolean = false
|
override fun forceUsageOfOpusEncoder(): Boolean = false
|
||||||
override fun shouldStartDmOnFirstMessage(): Boolean = false
|
override fun shouldStartDmOnFirstMessage(): Boolean = false
|
||||||
|
override fun isNewAppLayoutEnabled(): Boolean = false
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
render(
|
render(
|
||||||
GlideApp.with(imageView),
|
GlideApp.with(imageView),
|
||||||
matrixItem,
|
matrixItem,
|
||||||
DrawableImageViewTarget(imageView)
|
DrawableImageViewTarget(imageView),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,7 +103,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
render(
|
render(
|
||||||
glideRequests,
|
glideRequests,
|
||||||
matrixItem,
|
matrixItem,
|
||||||
DrawableImageViewTarget(imageView)
|
DrawableImageViewTarget(imageView),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +123,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
val matrixItem = MatrixItem.UserItem(
|
val matrixItem = MatrixItem.UserItem(
|
||||||
// Need an id starting with @
|
// Need an id starting with @
|
||||||
id = "@${mappedContact.displayName}",
|
id = "@${mappedContact.displayName}",
|
||||||
displayName = mappedContact.displayName
|
displayName = mappedContact.displayName,
|
||||||
)
|
)
|
||||||
|
|
||||||
val placeholder = getPlaceholderDrawable(matrixItem)
|
val placeholder = getPlaceholderDrawable(matrixItem)
|
||||||
@ -140,7 +140,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
val matrixItem = MatrixItem.UserItem(
|
val matrixItem = MatrixItem.UserItem(
|
||||||
// Need an id starting with @
|
// Need an id starting with @
|
||||||
id = profileInfo.matrixId,
|
id = profileInfo.matrixId,
|
||||||
displayName = profileInfo.displayName
|
displayName = profileInfo.displayName,
|
||||||
)
|
)
|
||||||
|
|
||||||
val placeholder = getPlaceholderDrawable(matrixItem)
|
val placeholder = getPlaceholderDrawable(matrixItem)
|
||||||
@ -215,7 +215,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
.bold()
|
.bold()
|
||||||
.endConfig()
|
.endConfig()
|
||||||
.buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor)
|
.buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor)
|
||||||
.toBitmap(width = iconSize, height = iconSize)
|
.toBitmap(width = iconSize, height = iconSize),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,7 +231,7 @@ class AvatarRenderer @Inject constructor(
|
|||||||
addPlaceholder: Boolean
|
addPlaceholder: Boolean
|
||||||
) {
|
) {
|
||||||
val transformations = mutableListOf<Transformation<Bitmap>>(
|
val transformations = mutableListOf<Transformation<Bitmap>>(
|
||||||
BlurTransformation(20, sampling)
|
BlurTransformation(20, sampling),
|
||||||
)
|
)
|
||||||
if (colorFilter != null) {
|
if (colorFilter != null) {
|
||||||
transformations.add(ColorFilterTransformation(colorFilter))
|
transformations.add(ColorFilterTransformation(colorFilter))
|
||||||
|
@ -84,6 +84,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||||||
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||||
object StopChatEffects : RoomDetailViewEvents()
|
object StopChatEffects : RoomDetailViewEvents()
|
||||||
object RoomReplacementStarted : RoomDetailViewEvents()
|
object RoomReplacementStarted : RoomDetailViewEvents()
|
||||||
|
|
||||||
data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents()
|
|
||||||
}
|
}
|
||||||
|
@ -75,7 +75,8 @@ data class RoomDetailViewState(
|
|||||||
val switchToParentSpace: Boolean = false,
|
val switchToParentSpace: Boolean = false,
|
||||||
val rootThreadEventId: String? = null,
|
val rootThreadEventId: String? = null,
|
||||||
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
|
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
|
||||||
val typingUsers: List<SenderInfo>? = null
|
val typingUsers: List<SenderInfo>? = null,
|
||||||
|
val isSharingLiveLocation: Boolean = false,
|
||||||
) : MavericksState {
|
) : MavericksState {
|
||||||
|
|
||||||
constructor(args: TimelineArgs) : this(
|
constructor(args: TimelineArgs) : this(
|
||||||
|
@ -498,7 +498,6 @@ class TimelineFragment @Inject constructor(
|
|||||||
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
||||||
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
||||||
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
|
||||||
is RoomDetailViewEvents.ChangeLocationIndicator -> handleChangeLocationIndicator(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -663,10 +662,6 @@ class TimelineFragment @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleChangeLocationIndicator(event: RoomDetailViewEvents.ChangeLocationIndicator) {
|
|
||||||
views.locationLiveStatusIndicator.isVisible = event.isVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) {
|
private fun displayErrorMessage(error: RoomDetailViewEvents.Failure) {
|
||||||
if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
|
if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
|
||||||
}
|
}
|
||||||
@ -1686,6 +1681,11 @@ class TimelineFragment @Inject constructor(
|
|||||||
} else if (mainState.asyncInviter.complete) {
|
} else if (mainState.asyncInviter.complete) {
|
||||||
vectorBaseActivity.finish()
|
vectorBaseActivity.finish()
|
||||||
}
|
}
|
||||||
|
updateLiveLocationIndicator(mainState.isSharingLiveLocation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
|
||||||
|
views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun FragmentTimelineBinding.hideComposerViews() {
|
private fun FragmentTimelineBinding.hideComposerViews() {
|
||||||
|
@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
|||||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||||
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
import im.vector.app.features.crypto.verification.SupportedVerificationMethodsProvider
|
||||||
|
import im.vector.app.features.home.room.detail.location.RedactLiveLocationShareEventUseCase
|
||||||
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
|
||||||
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
|
||||||
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
|
||||||
@ -105,6 +106,7 @@ import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper
|
|||||||
import org.matrix.android.sdk.api.session.room.read.ReadService
|
import org.matrix.android.sdk.api.session.room.read.ReadService
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
import org.matrix.android.sdk.api.session.room.timeline.Timeline
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.isLiveLocation
|
||||||
import org.matrix.android.sdk.api.session.sync.SyncRequestState
|
import org.matrix.android.sdk.api.session.sync.SyncRequestState
|
||||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationBadgeState
|
||||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||||
@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
private val notificationDrawerManager: NotificationDrawerManager,
|
private val notificationDrawerManager: NotificationDrawerManager,
|
||||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||||
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
||||||
|
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
|
||||||
timelineFactory: TimelineFactory,
|
timelineFactory: TimelineFactory,
|
||||||
appStateHandler: AppStateHandler,
|
appStateHandler: AppStateHandler,
|
||||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||||
@ -770,8 +773,14 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
|
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
|
||||||
val event = room.getTimelineEvent(action.targetEventId) ?: return
|
val event = room.getTimelineEvent(action.targetEventId) ?: return
|
||||||
|
if (event.isLiveLocation()) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
room.sendService().redactEvent(event.root, action.reason)
|
room.sendService().redactEvent(event.root, action.reason)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
|
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@ -1294,12 +1303,12 @@ class TimelineViewModel @AssistedInject constructor(
|
|||||||
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
|
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationServiceRunning() {
|
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||||
_viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = true))
|
setState { copy(isSharingLiveLocation = roomId in roomIds) }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationServiceStopped() {
|
override fun onLocationServiceStopped() {
|
||||||
_viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = false))
|
setState { copy(isSharingLiveLocation = false) }
|
||||||
// Bind again in case user decides to share live location without leaving the room
|
// Bind again in case user decides to share live location without leaving the room
|
||||||
locationSharingServiceConnection.bind(this)
|
locationSharingServiceConnection.bind(this)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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.home.room.detail.location
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
import org.matrix.android.sdk.api.session.room.Room
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class RedactLiveLocationShareEventUseCase @Inject constructor() {
|
||||||
|
|
||||||
|
suspend fun execute(event: Event, room: Room, reason: String?) {
|
||||||
|
event.eventId
|
||||||
|
?.takeUnless { it.isEmpty() }
|
||||||
|
?.let { room.locationSharingService().redactLiveLocationShare(it, reason) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
* 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.home.room.detail.timeline.action
|
||||||
|
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.EventType
|
||||||
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CheckIfCanRedactEventUseCase @Inject constructor(
|
||||||
|
private val activeSessionHolder: ActiveSessionHolder
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun execute(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||||
|
// Only some event types are supported for the moment
|
||||||
|
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) +
|
||||||
|
EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
|
||||||
|
|
||||||
|
return event.root.getClearType() in canRedactEventTypes &&
|
||||||
|
// Message sent by the current user can always be redacted, else check permission for messages sent by other users
|
||||||
|
(event.root.senderId == activeSessionHolder.getActiveSession().myUserId || actionPermissions.canRedact)
|
||||||
|
}
|
||||||
|
}
|
@ -82,6 +82,7 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||||||
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
private val pillsPostProcessorFactory: PillsPostProcessor.Factory,
|
||||||
private val vectorPreferences: VectorPreferences,
|
private val vectorPreferences: VectorPreferences,
|
||||||
private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase,
|
private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase,
|
||||||
|
private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase,
|
||||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||||
|
|
||||||
private val informationData = initialState.informationData
|
private val informationData = initialState.informationData
|
||||||
@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
private fun canRedact(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||||
// Only event of type EventType.MESSAGE, EventType.STICKER and EventType.POLL_START are supported for the moment
|
return checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||||
if (event.root.getClearType() !in listOf(EventType.MESSAGE, EventType.STICKER) + EventType.POLL_START) return false
|
|
||||||
// Message sent by the current user can always be redacted
|
|
||||||
if (event.root.senderId == session.myUserId) return true
|
|
||||||
// Check permission for messages sent by other users
|
|
||||||
return actionPermissions.canRedact
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||||
|
@ -36,7 +36,6 @@ import im.vector.app.features.location.toLocationData
|
|||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import org.threeten.bp.LocalDateTime
|
import org.threeten.bp.LocalDateTime
|
||||||
import timber.log.Timber
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LiveLocationShareMessageItemFactory @Inject constructor(
|
class LiveLocationShareMessageItemFactory @Inject constructor(
|
||||||
@ -135,7 +134,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
|
|||||||
liveLocationShareSummaryData.lastGeoUri.orEmpty(),
|
liveLocationShareSummaryData.lastGeoUri.orEmpty(),
|
||||||
getEndOfLiveDateTime(liveLocationShareSummaryData)
|
getEndOfLiveDateTime(liveLocationShareSummaryData)
|
||||||
)
|
)
|
||||||
}.also { viewState -> Timber.d("computed viewState: $viewState") }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
|
private fun getEndOfLiveDateTime(liveLocationShareSummaryData: LiveLocationShareSummaryData): LocalDateTime? {
|
||||||
|
@ -24,7 +24,6 @@ import im.vector.app.features.home.room.detail.timeline.TimelineEventController
|
|||||||
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
|
|
||||||
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
|
||||||
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
import im.vector.app.features.home.room.detail.timeline.item.MergedRoomCreationItem
|
||||||
@ -35,6 +34,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
|
|||||||
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
import org.matrix.android.sdk.api.crypto.MXCRYPTO_ALGORITHM_MEGOLM
|
||||||
import org.matrix.android.sdk.api.extensions.orFalse
|
import org.matrix.android.sdk.api.extensions.orFalse
|
||||||
import org.matrix.android.sdk.api.query.QueryStringValue
|
import org.matrix.android.sdk.api.query.QueryStringValue
|
||||||
|
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.content.EncryptionEventContent
|
import org.matrix.android.sdk.api.session.events.model.content.EncryptionEventContent
|
||||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||||
@ -53,6 +53,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper
|
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val mergeableEventTypes = listOf(EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_SERVER_ACL)
|
||||||
private val collapsedEventIds = linkedSetOf<Long>()
|
private val collapsedEventIds = linkedSetOf<Long>()
|
||||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||||
|
|
||||||
@ -78,19 +79,65 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
callback: TimelineEventController.Callback?,
|
callback: TimelineEventController.Callback?,
|
||||||
requestModelBuild: () -> Unit
|
requestModelBuild: () -> Unit
|
||||||
): BasedMergedItem<*>? {
|
): BasedMergedItem<*>? {
|
||||||
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
return when {
|
||||||
event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
|
isStartOfRoomCreationSummary(event, nextEvent) ->
|
||||||
// It's the first item before room.create
|
|
||||||
// Collapse all room configuration events
|
|
||||||
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) ->
|
||||||
null
|
buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
} else {
|
isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) ->
|
||||||
buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
buildRedactedEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildMembershipEventsMergedSummary(
|
/**
|
||||||
|
* @param event the main timeline event
|
||||||
|
* @param nextEvent is an older event than event
|
||||||
|
*/
|
||||||
|
private fun isStartOfRoomCreationSummary(
|
||||||
|
event: TimelineEvent,
|
||||||
|
nextEvent: TimelineEvent?,
|
||||||
|
): Boolean {
|
||||||
|
// It's the first item before room.create
|
||||||
|
// Collapse all room configuration events
|
||||||
|
return nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||||
|
event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param event the main timeline event
|
||||||
|
* @param nextEvent is an older event than event
|
||||||
|
* @param addDaySeparator true to add a day separator
|
||||||
|
*/
|
||||||
|
private fun isStartOfSameTypeEventsSummary(
|
||||||
|
event: TimelineEvent,
|
||||||
|
nextEvent: TimelineEvent?,
|
||||||
|
addDaySeparator: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
return event.root.getClearType() in mergeableEventTypes &&
|
||||||
|
(nextEvent?.root?.getClearType() != event.root.getClearType() || addDaySeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param event the main timeline event
|
||||||
|
* @param items all known items, sorted from newer event to oldest event
|
||||||
|
* @param currentPosition the current position
|
||||||
|
* @param addDaySeparator true to add a day separator
|
||||||
|
*/
|
||||||
|
private fun isStartOfRedactedEventsSummary(
|
||||||
|
event: TimelineEvent,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
currentPosition: Int,
|
||||||
|
addDaySeparator: Boolean,
|
||||||
|
): Boolean {
|
||||||
|
val nextNonRedactionEvent = items
|
||||||
|
.subList(fromIndex = currentPosition + 1, toIndex = items.size)
|
||||||
|
.find { it.root.getClearType() != EventType.REDACTION }
|
||||||
|
return event.root.isRedacted() &&
|
||||||
|
(!nextNonRedactionEvent?.root?.isRedacted().orFalse() || addDaySeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSameTypeEventsMergedSummary(
|
||||||
currentPosition: Int,
|
currentPosition: Int,
|
||||||
items: List<TimelineEvent>,
|
items: List<TimelineEvent>,
|
||||||
partialState: TimelineEventController.PartialState,
|
partialState: TimelineEventController.PartialState,
|
||||||
@ -102,11 +149,42 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
|
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
|
||||||
items,
|
items,
|
||||||
currentPosition,
|
currentPosition,
|
||||||
2,
|
MIN_NUMBER_OF_MERGED_EVENTS,
|
||||||
eventIdToHighlight,
|
eventIdToHighlight,
|
||||||
partialState.rootThreadEventId,
|
partialState.rootThreadEventId,
|
||||||
partialState.isFromThreadTimeline()
|
partialState.isFromThreadTimeline()
|
||||||
)
|
)
|
||||||
|
return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildRedactedEventsMergedSummary(
|
||||||
|
currentPosition: Int,
|
||||||
|
items: List<TimelineEvent>,
|
||||||
|
partialState: TimelineEventController.PartialState,
|
||||||
|
event: TimelineEvent,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
|
requestModelBuild: () -> Unit,
|
||||||
|
callback: TimelineEventController.Callback?
|
||||||
|
): MergedSimilarEventsItem_? {
|
||||||
|
val mergedEvents = timelineEventVisibilityHelper.prevRedactedEvents(
|
||||||
|
items,
|
||||||
|
currentPosition,
|
||||||
|
MIN_NUMBER_OF_MERGED_EVENTS,
|
||||||
|
eventIdToHighlight,
|
||||||
|
partialState.rootThreadEventId,
|
||||||
|
partialState.isFromThreadTimeline()
|
||||||
|
)
|
||||||
|
return buildSimilarEventsMergedSummary(mergedEvents, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildSimilarEventsMergedSummary(
|
||||||
|
mergedEvents: List<TimelineEvent>,
|
||||||
|
partialState: TimelineEventController.PartialState,
|
||||||
|
event: TimelineEvent,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
|
requestModelBuild: () -> Unit,
|
||||||
|
callback: TimelineEventController.Callback?
|
||||||
|
): MergedSimilarEventsItem_? {
|
||||||
return if (mergedEvents.isEmpty()) {
|
return if (mergedEvents.isEmpty()) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
@ -127,7 +205,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
)
|
)
|
||||||
mergedData.add(data)
|
mergedData.add(data)
|
||||||
}
|
}
|
||||||
val mergedEventIds = mergedEvents.map { it.localId }
|
val mergedEventIds = mergedEvents.map { it.localId }.toSet()
|
||||||
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
// We try to find if one of the item id were used as mergeItemCollapseStates key
|
||||||
// => handle case where paginating from mergeable events and we get more
|
// => handle case where paginating from mergeable events and we get more
|
||||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||||
@ -140,12 +218,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
collapsedEventIds.removeAll(mergedEventIds)
|
collapsedEventIds.removeAll(mergedEventIds)
|
||||||
}
|
}
|
||||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||||
val summaryTitleResId = when (event.root.getClearType()) {
|
getSummaryTitleResId(event.root)?.let { summaryTitle ->
|
||||||
EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
|
|
||||||
EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
summaryTitleResId?.let { summaryTitle ->
|
|
||||||
val attributes = MergedSimilarEventsItem.Attributes(
|
val attributes = MergedSimilarEventsItem.Attributes(
|
||||||
summaryTitleResId = summaryTitle,
|
summaryTitleResId = summaryTitle,
|
||||||
isCollapsed = isCollapsed,
|
isCollapsed = isCollapsed,
|
||||||
@ -168,6 +241,16 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getSummaryTitleResId(event: Event): Int? {
|
||||||
|
val type = event.getClearType()
|
||||||
|
return when {
|
||||||
|
type == EventType.STATE_ROOM_MEMBER -> R.plurals.membership_changes
|
||||||
|
type == EventType.STATE_ROOM_SERVER_ACL -> R.plurals.notice_room_server_acl_changes
|
||||||
|
event.isRedacted() -> R.plurals.room_removed_messages
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun buildRoomCreationMergedSummary(
|
private fun buildRoomCreationMergedSummary(
|
||||||
currentPosition: Int,
|
currentPosition: Int,
|
||||||
items: List<TimelineEvent>,
|
items: List<TimelineEvent>,
|
||||||
@ -191,7 +274,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
tmpPos--
|
tmpPos--
|
||||||
prevEvent = items.getOrNull(tmpPos)
|
prevEvent = items.getOrNull(tmpPos)
|
||||||
}
|
}
|
||||||
return if (mergedEvents.size > 2) {
|
return if (mergedEvents.size > MIN_NUMBER_OF_MERGED_EVENTS) {
|
||||||
var highlighted = false
|
var highlighted = false
|
||||||
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||||
mergedEvents.reversed()
|
mergedEvents.reversed()
|
||||||
@ -264,4 +347,8 @@ class MergedHeaderItemFactory @Inject constructor(
|
|||||||
fun isCollapsed(localId: Long): Boolean {
|
fun isCollapsed(localId: Long): Boolean {
|
||||||
return collapsedEventIds.contains(localId)
|
return collapsedEventIds.contains(localId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val MIN_NUMBER_OF_MERGED_EVENTS = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -113,8 +113,14 @@ class TimelineItemFactory @Inject constructor(
|
|||||||
EventType.CALL_NEGOTIATE,
|
EventType.CALL_NEGOTIATE,
|
||||||
EventType.REACTION,
|
EventType.REACTION,
|
||||||
in EventType.POLL_RESPONSE,
|
in EventType.POLL_RESPONSE,
|
||||||
in EventType.POLL_END,
|
in EventType.POLL_END -> noticeItemFactory.create(params)
|
||||||
in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params)
|
in EventType.BEACON_LOCATION_DATA -> {
|
||||||
|
if (event.root.isRedacted()) {
|
||||||
|
messageItemFactory.create(params)
|
||||||
|
} else {
|
||||||
|
noticeItemFactory.create(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
// Calls
|
// Calls
|
||||||
EventType.CALL_INVITE,
|
EventType.CALL_INVITE,
|
||||||
EventType.CALL_HANGUP,
|
EventType.CALL_HANGUP,
|
||||||
|
@ -51,12 +51,7 @@ object TimelineDisplayableEvents {
|
|||||||
EventType.STATE_ROOM_JOIN_RULES,
|
EventType.STATE_ROOM_JOIN_RULES,
|
||||||
EventType.KEY_VERIFICATION_DONE,
|
EventType.KEY_VERIFICATION_DONE,
|
||||||
EventType.KEY_VERIFICATION_CANCEL,
|
EventType.KEY_VERIFICATION_CANCEL,
|
||||||
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
|
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
|
||||||
}
|
|
||||||
|
|
||||||
fun TimelineEvent.canBeMerged(): Boolean {
|
|
||||||
return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
|
||||||
root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
fun TimelineEvent.isRoomConfiguration(roomCreatorUserId: String?): Boolean {
|
||||||
|
@ -18,6 +18,7 @@ package im.vector.app.features.home.room.detail.timeline.helper
|
|||||||
|
|
||||||
import im.vector.app.core.extensions.localDateTime
|
import im.vector.app.core.extensions.localDateTime
|
||||||
import im.vector.app.core.resources.UserPreferencesProvider
|
import im.vector.app.core.resources.UserPreferencesProvider
|
||||||
|
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.RelationType
|
import org.matrix.android.sdk.api.session.events.model.RelationType
|
||||||
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
import org.matrix.android.sdk.api.session.events.model.getRelationContent
|
||||||
@ -30,25 +31,38 @@ import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
|
|||||||
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class TimelineEventVisibilityHelper @Inject constructor(private val userPreferencesProvider: UserPreferencesProvider) {
|
class TimelineEventVisibilityHelper @Inject constructor(
|
||||||
|
private val userPreferencesProvider: UserPreferencesProvider,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private interface PredicateToStopSearch {
|
||||||
|
/**
|
||||||
|
* Indicate whether a search on events should stop by comparing 2 given successive events.
|
||||||
|
* @param oldEvent the oldest event between the 2 events to compare
|
||||||
|
* @param newEvent the more recent event between the 2 events to compare
|
||||||
|
*/
|
||||||
|
fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param timelineEvents the events to search in
|
* @param timelineEvents the events to search in, sorted from oldest event to newer event
|
||||||
* @param index the index to start computing (inclusive)
|
* @param index the index to start computing (inclusive)
|
||||||
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
||||||
* @param eventIdToHighlight used to compute visibility
|
* @param eventIdToHighlight used to compute visibility
|
||||||
* @param rootThreadEventId the root thread event id if in a thread timeline
|
* @param rootThreadEventId the root thread event id if in a thread timeline
|
||||||
* @param isFromThreadTimeline true if the timeline is a thread
|
* @param isFromThreadTimeline true if the timeline is a thread
|
||||||
|
* @param predicateToStop events are taken until this condition is met
|
||||||
*
|
*
|
||||||
* @return a list of timeline events which have sequentially the same type following the next direction.
|
* @return a list of timeline events which meet sequentially the same criteria following the next direction.
|
||||||
*/
|
*/
|
||||||
private fun nextSameTypeEvents(
|
private fun nextEventsUntil(
|
||||||
timelineEvents: List<TimelineEvent>,
|
timelineEvents: List<TimelineEvent>,
|
||||||
index: Int,
|
index: Int,
|
||||||
minSize: Int,
|
minSize: Int,
|
||||||
eventIdToHighlight: String?,
|
eventIdToHighlight: String?,
|
||||||
rootThreadEventId: String?,
|
rootThreadEventId: String?,
|
||||||
isFromThreadTimeline: Boolean
|
isFromThreadTimeline: Boolean,
|
||||||
|
predicateToStop: PredicateToStopSearch
|
||||||
): List<TimelineEvent> {
|
): List<TimelineEvent> {
|
||||||
if (index >= timelineEvents.size - 1) {
|
if (index >= timelineEvents.size - 1) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
@ -65,13 +79,15 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||||||
} else {
|
} else {
|
||||||
nextSubList.subList(0, indexOfNextDay)
|
nextSubList.subList(0, indexOfNextDay)
|
||||||
}
|
}
|
||||||
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
|
val indexOfFirstDifferentEvent = nextSameDayEvents.indexOfFirst {
|
||||||
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
predicateToStop.shouldStopSearch(oldEvent = timelineEvent.root, newEvent = it.root)
|
||||||
|
}
|
||||||
|
val similarEvents = if (indexOfFirstDifferentEvent == -1) {
|
||||||
nextSameDayEvents
|
nextSameDayEvents
|
||||||
} else {
|
} else {
|
||||||
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
|
nextSameDayEvents.subList(0, indexOfFirstDifferentEvent)
|
||||||
}
|
}
|
||||||
val filteredSameTypeEvents = sameTypeEvents.filter {
|
val filteredSimilarEvents = similarEvents.filter {
|
||||||
shouldShowEvent(
|
shouldShowEvent(
|
||||||
timelineEvent = it,
|
timelineEvent = it,
|
||||||
highlightedEventId = eventIdToHighlight,
|
highlightedEventId = eventIdToHighlight,
|
||||||
@ -79,14 +95,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||||||
rootThreadEventId = rootThreadEventId
|
rootThreadEventId = rootThreadEventId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (filteredSameTypeEvents.size < minSize) {
|
return if (filteredSimilarEvents.size < minSize) emptyList() else filteredSimilarEvents
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
return filteredSameTypeEvents
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param timelineEvents the events to search in
|
* @param timelineEvents the events to search in, sorted from newer event to oldest event
|
||||||
* @param index the index to start computing (inclusive)
|
* @param index the index to start computing (inclusive)
|
||||||
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
||||||
* @param eventIdToHighlight used to compute visibility
|
* @param eventIdToHighlight used to compute visibility
|
||||||
@ -107,7 +120,44 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||||||
return prevSub
|
return prevSub
|
||||||
.reversed()
|
.reversed()
|
||||||
.let {
|
.let {
|
||||||
nextSameTypeEvents(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline)
|
nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch {
|
||||||
|
override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean {
|
||||||
|
return oldEvent.getClearType() != newEvent.getClearType()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param timelineEvents the events to search in, sorted from newer event to oldest event
|
||||||
|
* @param index the index to start computing (inclusive)
|
||||||
|
* @param minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
||||||
|
* @param eventIdToHighlight used to compute visibility
|
||||||
|
* @param rootThreadEventId the root thread eventId
|
||||||
|
* @param isFromThreadTimeline true if the timeline is a thread
|
||||||
|
*
|
||||||
|
* @return a list of timeline events which are all redacted following the prev direction.
|
||||||
|
*/
|
||||||
|
fun prevRedactedEvents(
|
||||||
|
timelineEvents: List<TimelineEvent>,
|
||||||
|
index: Int,
|
||||||
|
minSize: Int,
|
||||||
|
eventIdToHighlight: String?,
|
||||||
|
rootThreadEventId: String?,
|
||||||
|
isFromThreadTimeline: Boolean
|
||||||
|
): List<TimelineEvent> {
|
||||||
|
val prevSub = timelineEvents
|
||||||
|
.subList(0, index + 1)
|
||||||
|
// Ensure to not take the REDACTION events into account
|
||||||
|
.filter { it.root.getClearType() != EventType.REDACTION }
|
||||||
|
return prevSub
|
||||||
|
.reversed()
|
||||||
|
.let {
|
||||||
|
nextEventsUntil(it, 0, minSize, eventIdToHighlight, rootThreadEventId, isFromThreadTimeline, object : PredicateToStopSearch {
|
||||||
|
override fun shouldStopSearch(oldEvent: Event, newEvent: Event): Boolean {
|
||||||
|
return oldEvent.isRedacted() && !newEvent.isRedacted()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,6 +241,10 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
|||||||
} else root.eventId != rootThreadEventId
|
} else root.eventId != rootThreadEventId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (root.getClearType() in EventType.BEACON_LOCATION_DATA) {
|
||||||
|
return !root.isRedacted()
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,8 +84,6 @@ class UpgradeRoomViewModelTask @Inject constructor(
|
|||||||
// autoJoin = currentInfo.autoJoin ?: false,
|
// autoJoin = currentInfo.autoJoin ?: false,
|
||||||
suggested = currentInfo.suggested
|
suggested = currentInfo.suggested
|
||||||
)
|
)
|
||||||
|
|
||||||
parentSpace.removeChildren(params.roomId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (failure: Throwable) {
|
} catch (failure: Throwable) {
|
||||||
|
@ -26,9 +26,12 @@ import im.vector.app.core.di.ActiveSessionHolder
|
|||||||
import im.vector.app.core.services.VectorAndroidService
|
import im.vector.app.core.services.VectorAndroidService
|
||||||
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
|
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
|
||||||
import im.vector.app.features.notifications.NotificationUtils
|
import im.vector.app.features.notifications.NotificationUtils
|
||||||
|
import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase
|
||||||
import im.vector.app.features.session.coroutineScope
|
import im.vector.app.features.session.coroutineScope
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
@ -55,6 +58,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
|||||||
@Inject lateinit var locationTracker: LocationTracker
|
@Inject lateinit var locationTracker: LocationTracker
|
||||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||||
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
|
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
|
||||||
|
@Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase
|
||||||
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
@ -66,6 +70,9 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
|||||||
private val jobs = mutableListOf<Job>()
|
private val jobs = mutableListOf<Job>()
|
||||||
private var startInProgress = false
|
private var startInProgress = false
|
||||||
|
|
||||||
|
private val _roomIdsOfActiveLives = MutableSharedFlow<Set<String>>(replay = 1)
|
||||||
|
val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Timber.i("onCreate")
|
Timber.i("onCreate")
|
||||||
@ -193,24 +200,30 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
|||||||
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
|
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
|
||||||
Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
|
Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
|
||||||
roomArgsMap[beaconEventId] = roomArgs
|
roomArgsMap[beaconEventId] = roomArgs
|
||||||
|
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun removeRoomArgs(beaconEventId: String) {
|
private fun removeRoomArgs(beaconEventId: String) {
|
||||||
Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
|
Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
|
||||||
roomArgsMap.remove(beaconEventId)
|
roomArgsMap.remove(beaconEventId)
|
||||||
|
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) {
|
private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) {
|
||||||
launchWithActiveSession { session ->
|
launchWithActiveSession { session ->
|
||||||
val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId)
|
val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId)
|
||||||
.distinctUntilChangedBy { it.isActive }
|
.distinctUntilChangedBy { it?.isActive }
|
||||||
.filter { it.isActive == false }
|
.filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) }
|
||||||
.onEach { stopSharingLocation(beaconEventId) }
|
.onEach { stopSharingLocation(beaconEventId) }
|
||||||
.launchIn(session.coroutineScope)
|
.launchIn(session.coroutineScope)
|
||||||
jobs.add(job)
|
jobs.add(job)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun isLiveRedacted(roomId: String, beaconEventId: String): Boolean {
|
||||||
|
return checkIfEventIsRedactedUseCase.execute(roomId = roomId, eventId = beaconEventId)
|
||||||
|
}
|
||||||
|
|
||||||
private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) =
|
private fun launchWithActiveSession(block: suspend CoroutineScope.(Session) -> Unit) =
|
||||||
activeSessionHolder
|
activeSessionHolder
|
||||||
.getSafeActiveSession()
|
.getSafeActiveSession()
|
||||||
@ -220,6 +233,10 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRoomIdsOfActiveLives(): Set<String> {
|
||||||
|
return roomArgsMap.map { it.value.roomId }.toSet()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder {
|
override fun onBind(intent: Intent?): IBinder {
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
|
@ -21,17 +21,22 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.ServiceConnection
|
import android.content.ServiceConnection
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
|
import im.vector.app.features.session.coroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class LocationSharingServiceConnection @Inject constructor(
|
class LocationSharingServiceConnection @Inject constructor(
|
||||||
private val context: Context
|
private val context: Context,
|
||||||
) : ServiceConnection,
|
private val activeSessionHolder: ActiveSessionHolder
|
||||||
LocationSharingAndroidService.Callback {
|
) : ServiceConnection, LocationSharingAndroidService.Callback {
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun onLocationServiceRunning()
|
fun onLocationServiceRunning(roomIds: Set<String>)
|
||||||
fun onLocationServiceStopped()
|
fun onLocationServiceStopped()
|
||||||
fun onLocationServiceError(error: Throwable)
|
fun onLocationServiceError(error: Throwable)
|
||||||
}
|
}
|
||||||
@ -44,7 +49,7 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||||||
addCallback(callback)
|
addCallback(callback)
|
||||||
|
|
||||||
if (isBound) {
|
if (isBound) {
|
||||||
callback.onLocationServiceRunning()
|
callback.onLocationServiceRunning(getRoomIdsOfActiveLives())
|
||||||
} else {
|
} else {
|
||||||
Intent(context, LocationSharingAndroidService::class.java).also { intent ->
|
Intent(context, LocationSharingAndroidService::class.java).also { intent ->
|
||||||
context.bindService(intent, this, 0)
|
context.bindService(intent, this, 0)
|
||||||
@ -56,12 +61,24 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||||||
removeCallback(callback)
|
removeCallback(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getRoomIdsOfActiveLives(): Set<String> {
|
||||||
|
return locationSharingAndroidService?.getRoomIdsOfActiveLives() ?: emptySet()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
||||||
locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also {
|
locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { service ->
|
||||||
it.callback = this
|
service.callback = this
|
||||||
|
getActiveSessionCoroutineScope()?.let { scope ->
|
||||||
|
service.roomIdsOfActiveLives
|
||||||
|
.onEach(::onRoomIdsUpdate)
|
||||||
|
.launchIn(scope)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
isBound = true
|
isBound = true
|
||||||
onCallbackActionNoArg(Callback::onLocationServiceRunning)
|
}
|
||||||
|
|
||||||
|
private fun getActiveSessionCoroutineScope(): CoroutineScope? {
|
||||||
|
return activeSessionHolder.getSafeActiveSession()?.coroutineScope
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(className: ComponentName) {
|
override fun onServiceDisconnected(className: ComponentName) {
|
||||||
@ -71,6 +88,10 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||||||
onCallbackActionNoArg(Callback::onLocationServiceStopped)
|
onCallbackActionNoArg(Callback::onLocationServiceStopped)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onRoomIdsUpdate(roomIds: Set<String>) {
|
||||||
|
forwardRoomIdsToCallbacks(roomIds)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onServiceError(error: Throwable) {
|
override fun onServiceError(error: Throwable) {
|
||||||
forwardErrorToCallbacks(error)
|
forwardErrorToCallbacks(error)
|
||||||
}
|
}
|
||||||
@ -87,6 +108,10 @@ class LocationSharingServiceConnection @Inject constructor(
|
|||||||
callbacks.toList().forEach(action)
|
callbacks.toList().forEach(action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun forwardRoomIdsToCallbacks(roomIds: Set<String>) {
|
||||||
|
callbacks.toList().forEach { it.onLocationServiceRunning(roomIds) }
|
||||||
|
}
|
||||||
|
|
||||||
private fun forwardErrorToCallbacks(error: Throwable) {
|
private fun forwardErrorToCallbacks(error: Throwable) {
|
||||||
callbacks.toList().forEach { it.onLocationServiceError(error) }
|
callbacks.toList().forEach { it.onLocationServiceError(error) }
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ package im.vector.app.features.location.live
|
|||||||
import androidx.lifecycle.asFlow
|
import androidx.lifecycle.asFlow
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.mapNotNull
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
import org.matrix.android.sdk.api.session.getRoom
|
import org.matrix.android.sdk.api.session.getRoom
|
||||||
@ -31,13 +31,13 @@ class GetLiveLocationShareSummaryUseCase @Inject constructor(
|
|||||||
private val session: Session,
|
private val session: Session,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(roomId: String, eventId: String): Flow<LiveLocationShareAggregatedSummary> = withContext(session.coroutineDispatchers.main) {
|
suspend fun execute(roomId: String, eventId: String): Flow<LiveLocationShareAggregatedSummary?> = withContext(session.coroutineDispatchers.main) {
|
||||||
Timber.d("getting flow for roomId=$roomId and eventId=$eventId")
|
Timber.d("getting flow for roomId=$roomId and eventId=$eventId")
|
||||||
session.getRoom(roomId)
|
session.getRoom(roomId)
|
||||||
?.locationSharingService()
|
?.locationSharingService()
|
||||||
?.getLiveLocationShareSummary(eventId)
|
?.getLiveLocationShareSummary(eventId)
|
||||||
?.asFlow()
|
?.asFlow()
|
||||||
?.mapNotNull { it.getOrNull() }
|
?.map { it.getOrNull() }
|
||||||
?: emptyFlow()
|
?: emptyFlow()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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.live.map
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.PopupWindow
|
||||||
|
import im.vector.app.R
|
||||||
|
import im.vector.app.databinding.ViewLiveLocationMarkerPopupBinding
|
||||||
|
|
||||||
|
class LocationLiveMapMarkerOptionsDialog(
|
||||||
|
context: Context,
|
||||||
|
) : PopupWindow() {
|
||||||
|
|
||||||
|
interface Callback {
|
||||||
|
fun onShareLocationClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val views: ViewLiveLocationMarkerPopupBinding
|
||||||
|
|
||||||
|
var callback: Callback? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
contentView = View.inflate(context, R.layout.view_live_location_marker_popup, null)
|
||||||
|
|
||||||
|
views = ViewLiveLocationMarkerPopupBinding.bind(contentView)
|
||||||
|
|
||||||
|
width = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
|
inputMethodMode = INPUT_METHOD_NOT_NEEDED
|
||||||
|
isFocusable = true
|
||||||
|
isTouchable = true
|
||||||
|
|
||||||
|
contentView.setOnClickListener {
|
||||||
|
callback?.onShareLocationClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun show(anchorView: View) {
|
||||||
|
contentView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED)
|
||||||
|
// By default the left side of the dialog is aligned with the pin. We need shift it to the left to make it's center aligned with the pin.
|
||||||
|
showAsDropDown(anchorView, -contentView.measuredWidth / 2, 0)
|
||||||
|
}
|
||||||
|
}
|
@ -33,6 +33,7 @@ import com.mapbox.mapboxsdk.maps.MapboxMap
|
|||||||
import com.mapbox.mapboxsdk.maps.MapboxMapOptions
|
import com.mapbox.mapboxsdk.maps.MapboxMapOptions
|
||||||
import com.mapbox.mapboxsdk.maps.Style
|
import com.mapbox.mapboxsdk.maps.Style
|
||||||
import com.mapbox.mapboxsdk.maps.SupportMapFragment
|
import com.mapbox.mapboxsdk.maps.SupportMapFragment
|
||||||
|
import com.mapbox.mapboxsdk.plugins.annotation.Symbol
|
||||||
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
|
||||||
@ -42,6 +43,7 @@ import im.vector.app.core.extensions.addChildFragment
|
|||||||
import im.vector.app.core.extensions.configureWith
|
import im.vector.app.core.extensions.configureWith
|
||||||
import im.vector.app.core.platform.VectorBaseFragment
|
import im.vector.app.core.platform.VectorBaseFragment
|
||||||
import im.vector.app.core.utils.DimensionConverter
|
import im.vector.app.core.utils.DimensionConverter
|
||||||
|
import im.vector.app.core.utils.openLocation
|
||||||
import im.vector.app.databinding.FragmentLocationLiveMapViewBinding
|
import im.vector.app.databinding.FragmentLocationLiveMapViewBinding
|
||||||
import im.vector.app.features.location.UrlMapProvider
|
import im.vector.app.features.location.UrlMapProvider
|
||||||
import im.vector.app.features.location.zoomToBounds
|
import im.vector.app.features.location.zoomToBounds
|
||||||
@ -120,6 +122,10 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||||||
this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap)
|
this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap)
|
||||||
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style).apply {
|
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style).apply {
|
||||||
iconAllowOverlap = true
|
iconAllowOverlap = true
|
||||||
|
addClickListener {
|
||||||
|
onSymbolClicked(it)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pendingLiveLocations
|
pendingLiveLocations
|
||||||
.takeUnless { it.isEmpty() }
|
.takeUnless { it.isEmpty() }
|
||||||
@ -129,6 +135,31 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onSymbolClicked(symbol: Symbol?) {
|
||||||
|
symbol?.let {
|
||||||
|
val screenLocation = mapboxMap?.get()?.projection?.toScreenLocation(it.latLng)
|
||||||
|
views.liveLocationPopupAnchor.apply {
|
||||||
|
x = screenLocation?.x ?: 0f
|
||||||
|
y = (screenLocation?.y ?: 0f) - views.liveLocationPopupAnchor.height
|
||||||
|
}
|
||||||
|
|
||||||
|
LocationLiveMapMarkerOptionsDialog(requireContext())
|
||||||
|
.apply {
|
||||||
|
callback = object : LocationLiveMapMarkerOptionsDialog.Callback {
|
||||||
|
override fun onShareLocationClicked() {
|
||||||
|
shareLocation(symbol)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.show(views.liveLocationPopupAnchor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shareLocation(symbol: Symbol) {
|
||||||
|
openLocation(requireActivity(), symbol.latLng.latitude, symbol.latLng.longitude)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getOrCreateSupportMapFragment() =
|
private fun getOrCreateSupportMapFragment() =
|
||||||
childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment
|
childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment
|
||||||
?: run {
|
?: run {
|
||||||
|
@ -87,7 +87,7 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocationServiceRunning() {
|
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||||
// NOOP
|
// NOOP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +52,13 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
|||||||
object ResendResetPassword : OnboardingAction
|
object ResendResetPassword : OnboardingAction
|
||||||
object ResetPasswordMailConfirmed : OnboardingAction
|
object ResetPasswordMailConfirmed : OnboardingAction
|
||||||
|
|
||||||
data class MaybeUpdateHomeserverFromMatrixId(val userId: String) : OnboardingAction
|
sealed interface UserNameEnteredAction : OnboardingAction {
|
||||||
|
data class Registration(val userId: String) : UserNameEnteredAction
|
||||||
|
data class Login(val userId: String) : UserNameEnteredAction
|
||||||
|
}
|
||||||
sealed interface AuthenticateAction : OnboardingAction {
|
sealed interface AuthenticateAction : OnboardingAction {
|
||||||
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
data class Register(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
|
data class RegisterWithMatrixId(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
data class Login(val username: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
data class LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||||
}
|
}
|
||||||
@ -71,6 +75,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
|||||||
object ResetSignMode : ResetAction
|
object ResetSignMode : ResetAction
|
||||||
object ResetAuthenticationAttempt : ResetAction
|
object ResetAuthenticationAttempt : ResetAction
|
||||||
object ResetResetPassword : ResetAction
|
object ResetResetPassword : ResetAction
|
||||||
|
object ResetSelectedRegistrationUserName : ResetAction
|
||||||
|
|
||||||
// Homeserver history
|
// Homeserver history
|
||||||
object ClearHomeServerHistory : OnboardingAction
|
object ClearHomeServerHistory : OnboardingAction
|
||||||
|
@ -28,6 +28,8 @@ import im.vector.app.core.di.hiltMavericksViewModelFactory
|
|||||||
import im.vector.app.core.extensions.cancelCurrentOnSet
|
import im.vector.app.core.extensions.cancelCurrentOnSet
|
||||||
import im.vector.app.core.extensions.configureAndStart
|
import im.vector.app.core.extensions.configureAndStart
|
||||||
import im.vector.app.core.extensions.inferNoConnectivity
|
import im.vector.app.core.extensions.inferNoConnectivity
|
||||||
|
import im.vector.app.core.extensions.isMatrixId
|
||||||
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.core.extensions.vectorStore
|
import im.vector.app.core.extensions.vectorStore
|
||||||
import im.vector.app.core.platform.VectorViewModel
|
import im.vector.app.core.platform.VectorViewModel
|
||||||
import im.vector.app.core.resources.BuildMeta
|
import im.vector.app.core.resources.BuildMeta
|
||||||
@ -57,6 +59,7 @@ import org.matrix.android.sdk.api.auth.HomeServerHistoryService
|
|||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||||
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
import org.matrix.android.sdk.api.auth.login.LoginWizard
|
||||||
|
import org.matrix.android.sdk.api.auth.registration.RegistrationAvailability
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
import org.matrix.android.sdk.api.auth.registration.RegistrationWizard
|
||||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||||
import org.matrix.android.sdk.api.session.Session
|
import org.matrix.android.sdk.api.session.Session
|
||||||
@ -144,7 +147,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||||
is OnboardingAction.InitWith -> handleInitWith(action)
|
is OnboardingAction.InitWith -> handleInitWith(action)
|
||||||
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
is OnboardingAction.HomeServerChange -> withAction(action) { handleHomeserverChange(action) }
|
||||||
is OnboardingAction.MaybeUpdateHomeserverFromMatrixId -> handleMaybeUpdateHomeserver(action)
|
is OnboardingAction.UserNameEnteredAction -> handleUserNameEntered(action)
|
||||||
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
is AuthenticateAction -> withAction(action) { handleAuthenticateAction(action) }
|
||||||
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
is OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||||
@ -167,13 +170,47 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleMaybeUpdateHomeserver(action: OnboardingAction.MaybeUpdateHomeserverFromMatrixId) {
|
private fun handleUserNameEntered(action: OnboardingAction.UserNameEnteredAction) {
|
||||||
val isFullMatrixId = MatrixPatterns.isUserId(action.userId)
|
when (action) {
|
||||||
|
is OnboardingAction.UserNameEnteredAction.Login -> maybeUpdateHomeserver(action.userId)
|
||||||
|
is OnboardingAction.UserNameEnteredAction.Registration -> maybeUpdateHomeserver(action.userId, continuation = { userName ->
|
||||||
|
checkUserNameAvailability(userName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun maybeUpdateHomeserver(userNameOrMatrixId: String, continuation: suspend (String) -> Unit = {}) {
|
||||||
|
val isFullMatrixId = MatrixPatterns.isUserId(userNameOrMatrixId)
|
||||||
if (isFullMatrixId) {
|
if (isFullMatrixId) {
|
||||||
val domain = action.userId.getServerName().substringBeforeLast(":").ensureProtocol()
|
val domain = userNameOrMatrixId.getServerName().substringBeforeLast(":").ensureProtocol()
|
||||||
handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain))
|
handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain), postAction = {
|
||||||
|
val userName = MatrixPatterns.extractUserNameFromId(userNameOrMatrixId) ?: throw IllegalStateException("unexpected non matrix id")
|
||||||
|
continuation(userName)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// ignore the action
|
currentJob = viewModelScope.launch { continuation(userNameOrMatrixId) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun checkUserNameAvailability(userName: String) {
|
||||||
|
when (val result = registrationWizard.registrationAvailable(userName)) {
|
||||||
|
RegistrationAvailability.Available -> {
|
||||||
|
setState {
|
||||||
|
copy(
|
||||||
|
registrationState = RegistrationState(
|
||||||
|
isUserNameAvailable = true,
|
||||||
|
selectedMatrixId = when {
|
||||||
|
userName.isMatrixId() -> userName
|
||||||
|
else -> "@$userName:${selectedHomeserver.userFacingUrl.toReducedUrl()}"
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is RegistrationAvailability.NotAvailable -> {
|
||||||
|
_viewEvents.post(OnboardingViewEvents.Failure(result.failure))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,7 +221,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
|
|
||||||
private fun handleAuthenticateAction(action: AuthenticateAction) {
|
private fun handleAuthenticateAction(action: AuthenticateAction) {
|
||||||
when (action) {
|
when (action) {
|
||||||
is AuthenticateAction.Register -> handleRegisterWith(action)
|
is AuthenticateAction.Register -> handleRegisterWith(action.username, action.password, action.initialDeviceName)
|
||||||
|
is AuthenticateAction.RegisterWithMatrixId -> handleRegisterWith(
|
||||||
|
MatrixPatterns.extractUserNameFromId(action.matrixId) ?: throw IllegalStateException("unexpected non matrix id"),
|
||||||
|
action.password,
|
||||||
|
action.initialDeviceName
|
||||||
|
)
|
||||||
is AuthenticateAction.Login -> handleLogin(action)
|
is AuthenticateAction.Login -> handleLogin(action)
|
||||||
is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null)
|
is AuthenticateAction.LoginDirect -> handleDirectLogin(action, homeServerConnectionConfig = null)
|
||||||
}
|
}
|
||||||
@ -322,17 +364,17 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleRegisterWith(action: AuthenticateAction.Register) {
|
private fun handleRegisterWith(userName: String, password: String, initialDeviceName: String) {
|
||||||
setState {
|
setState {
|
||||||
val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password)
|
val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password)
|
||||||
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
|
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
|
||||||
}
|
}
|
||||||
reAuthHelper.data = action.password
|
reAuthHelper.data = password
|
||||||
handleRegisterAction(
|
handleRegisterAction(
|
||||||
RegisterAction.CreateAccount(
|
RegisterAction.CreateAccount(
|
||||||
action.username,
|
userName,
|
||||||
action.password,
|
password,
|
||||||
action.initialDeviceName
|
initialDeviceName
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -368,7 +410,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
OnboardingAction.ResetAuthenticationAttempt -> {
|
OnboardingAction.ResetAuthenticationAttempt -> {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
authenticationService.cancelPendingLoginOrRegistration()
|
authenticationService.cancelPendingLoginOrRegistration()
|
||||||
setState { copy(isLoading = false) }
|
setState {
|
||||||
|
copy(
|
||||||
|
isLoading = false,
|
||||||
|
registrationState = RegistrationState(),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnboardingAction.ResetResetPassword -> {
|
OnboardingAction.ResetResetPassword -> {
|
||||||
@ -380,6 +427,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
OnboardingAction.ResetDeeplinkConfig -> loginConfig = null
|
OnboardingAction.ResetDeeplinkConfig -> loginConfig = null
|
||||||
|
OnboardingAction.ResetSelectedRegistrationUserName -> {
|
||||||
|
setState {
|
||||||
|
copy(registrationState = RegistrationState())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -593,6 +645,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
|
val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
|
||||||
val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull()
|
val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull()
|
||||||
state.personalizationState.copy(
|
state.personalizationState.copy(
|
||||||
|
displayName = state.registrationState.selectedMatrixId?.let { MatrixPatterns.extractUserNameFromId(it) },
|
||||||
supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName,
|
supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName,
|
||||||
supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar
|
supportsChangingProfilePicture = capabilityOverrides?.canChangeAvatar ?: homeServerCapabilities.canChangeAvatar
|
||||||
)
|
)
|
||||||
@ -619,27 +672,31 @@ class OnboardingViewModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null) {
|
private fun handleHomeserverChange(action: OnboardingAction.HomeServerChange, serverTypeOverride: ServerType? = null, postAction: suspend () -> Unit = {}) {
|
||||||
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
val homeServerConnectionConfig = homeServerConnectionConfigFactory.create(action.homeServerUrl)
|
||||||
if (homeServerConnectionConfig == null) {
|
if (homeServerConnectionConfig == null) {
|
||||||
// This is invalid
|
// This is invalid
|
||||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||||
} else {
|
} else {
|
||||||
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride)
|
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startAuthenticationFlow(
|
private fun startAuthenticationFlow(
|
||||||
trigger: OnboardingAction.HomeServerChange,
|
trigger: OnboardingAction.HomeServerChange,
|
||||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||||
serverTypeOverride: ServerType?
|
serverTypeOverride: ServerType?,
|
||||||
|
postAction: suspend () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||||
|
|
||||||
currentJob = viewModelScope.launch {
|
currentJob = viewModelScope.launch {
|
||||||
setState { copy(isLoading = true) }
|
setState { copy(isLoading = true) }
|
||||||
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
||||||
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
|
onSuccess = {
|
||||||
|
onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride)
|
||||||
|
postAction()
|
||||||
|
},
|
||||||
onFailure = { onAuthenticationStartError(it, trigger) }
|
onFailure = { onAuthenticationStartError(it, trigger) }
|
||||||
)
|
)
|
||||||
setState { copy(isLoading = false) }
|
setState { copy(isLoading = false) }
|
||||||
|
@ -48,6 +48,9 @@ data class OnboardingViewState(
|
|||||||
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
||||||
val isForceLoginFallbackEnabled: Boolean = false,
|
val isForceLoginFallbackEnabled: Boolean = false,
|
||||||
|
|
||||||
|
@PersistState
|
||||||
|
val registrationState: RegistrationState = RegistrationState(),
|
||||||
|
|
||||||
@PersistState
|
@PersistState
|
||||||
val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(),
|
val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(),
|
||||||
|
|
||||||
@ -66,7 +69,6 @@ enum class OnboardingFlow {
|
|||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class SelectedHomeserverState(
|
data class SelectedHomeserverState(
|
||||||
val description: String? = null,
|
|
||||||
val userFacingUrl: String? = null,
|
val userFacingUrl: String? = null,
|
||||||
val upstreamUrl: String? = null,
|
val upstreamUrl: String? = null,
|
||||||
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
||||||
@ -96,3 +98,9 @@ data class ResetState(
|
|||||||
data class SelectedAuthenticationState(
|
data class SelectedAuthenticationState(
|
||||||
val description: AuthenticationDescription? = null,
|
val description: AuthenticationDescription? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class RegistrationState(
|
||||||
|
val isUserNameAvailable: Boolean = false,
|
||||||
|
val selectedMatrixId: String? = null,
|
||||||
|
) : Parcelable
|
||||||
|
@ -16,10 +16,7 @@
|
|||||||
|
|
||||||
package im.vector.app.features.onboarding
|
package im.vector.app.features.onboarding
|
||||||
|
|
||||||
import im.vector.app.R
|
|
||||||
import im.vector.app.core.extensions.containsAllItems
|
import im.vector.app.core.extensions.containsAllItems
|
||||||
import im.vector.app.core.resources.StringProvider
|
|
||||||
import im.vector.app.core.utils.ensureTrailingSlash
|
|
||||||
import im.vector.app.features.login.LoginMode
|
import im.vector.app.features.login.LoginMode
|
||||||
import org.matrix.android.sdk.api.auth.AuthenticationService
|
import org.matrix.android.sdk.api.auth.AuthenticationService
|
||||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||||
@ -29,7 +26,6 @@ import javax.inject.Inject
|
|||||||
|
|
||||||
class StartAuthenticationFlowUseCase @Inject constructor(
|
class StartAuthenticationFlowUseCase @Inject constructor(
|
||||||
private val authenticationService: AuthenticationService,
|
private val authenticationService: AuthenticationService,
|
||||||
private val stringProvider: StringProvider
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun execute(config: HomeServerConnectionConfig): StartAuthenticationResult {
|
suspend fun execute(config: HomeServerConnectionConfig): StartAuthenticationResult {
|
||||||
@ -46,10 +42,6 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
|||||||
config: HomeServerConnectionConfig,
|
config: HomeServerConnectionConfig,
|
||||||
preferredLoginMode: LoginMode
|
preferredLoginMode: LoginMode
|
||||||
) = SelectedHomeserverState(
|
) = SelectedHomeserverState(
|
||||||
description = when (config.homeServerUri.toString()) {
|
|
||||||
matrixOrgUrl() -> stringProvider.getString(R.string.ftue_auth_create_account_matrix_dot_org_server_description)
|
|
||||||
else -> null
|
|
||||||
},
|
|
||||||
userFacingUrl = config.homeServerUri.toString(),
|
userFacingUrl = config.homeServerUri.toString(),
|
||||||
upstreamUrl = authFlow.homeServerUrl,
|
upstreamUrl = authFlow.homeServerUrl,
|
||||||
preferredLoginMode = preferredLoginMode,
|
preferredLoginMode = preferredLoginMode,
|
||||||
@ -57,8 +49,6 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
|||||||
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
|
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
|
||||||
|
|
||||||
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
||||||
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders)
|
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders)
|
||||||
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders)
|
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders)
|
||||||
|
@ -16,15 +16,18 @@
|
|||||||
|
|
||||||
package im.vector.app.features.onboarding.ftueauth
|
package im.vector.app.features.onboarding.ftueauth
|
||||||
|
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.text.toSpannable
|
||||||
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.animations.play
|
||||||
import im.vector.app.core.di.ActiveSessionHolder
|
import im.vector.app.core.di.ActiveSessionHolder
|
||||||
import im.vector.app.core.utils.isAnimationEnabled
|
import im.vector.app.core.utils.isAnimationEnabled
|
||||||
|
import im.vector.app.core.utils.styleMatchingText
|
||||||
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
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
@ -47,7 +50,9 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun setupViews() {
|
private fun setupViews() {
|
||||||
views.accountCreatedSubtitle.text = getString(R.string.ftue_account_created_subtitle, activeSessionHolder.getActiveSession().myUserId)
|
val userId = activeSessionHolder.getActiveSession().myUserId
|
||||||
|
val subtitle = getString(R.string.ftue_account_created_subtitle, userId).toSpannable().styleMatchingText(userId, Typeface.BOLD)
|
||||||
|
views.accountCreatedSubtitle.text = subtitle
|
||||||
views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) }
|
views.accountCreatedPersonalize.debouncedClicks { viewModel.handle(OnboardingAction.PersonalizeProfile) }
|
||||||
views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
views.accountCreatedTakeMeHome.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
||||||
views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
views.accountCreatedTakeMeHomeCta.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnTakeMeHome)) }
|
||||||
|
@ -25,6 +25,7 @@ import androidx.autofill.HintConstants
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
@ -41,8 +42,10 @@ import im.vector.app.features.login.render
|
|||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||||
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class FtueAuthCombinedLoginFragment @Inject constructor(
|
class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||||
@ -60,14 +63,18 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
|||||||
views.loginRoot.realignPercentagesToParent()
|
views.loginRoot.realignPercentagesToParent()
|
||||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
views.loginPasswordInput.setOnImeDoneListener { submit() }
|
||||||
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.loginInput.content())) }
|
views.loginInput.setOnFocusLostListener { viewModel.handle(OnboardingAction.UserNameEnteredAction.Login(views.loginInput.content())) }
|
||||||
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) }
|
views.loginForgotPassword.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnForgetPasswordClicked)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
views.loginSubmit.setOnClickListener { submit() }
|
views.loginSubmit.setOnClickListener { submit() }
|
||||||
observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit)
|
views.loginInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
views.loginPasswordInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
|
|
||||||
|
combine(views.loginInput.editText().textChanges(), views.loginPasswordInput.editText().textChanges()) { account, password ->
|
||||||
|
views.loginSubmit.isEnabled = account.isNotEmpty() && password.isNotEmpty()
|
||||||
|
}.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submit() {
|
private fun submit() {
|
||||||
@ -105,7 +112,6 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
|||||||
setupAutoFill()
|
setupAutoFill()
|
||||||
|
|
||||||
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||||
views.selectedServerDescription.text = state.selectedHomeserver.description
|
|
||||||
|
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
// Ensure password is hidden
|
// Ensure password is hidden
|
||||||
|
@ -28,11 +28,14 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import com.airbnb.mvrx.withState
|
import com.airbnb.mvrx.withState
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.hasSurroundingSpaces
|
import im.vector.app.core.extensions.hasSurroundingSpaces
|
||||||
import im.vector.app.core.extensions.hideKeyboard
|
import im.vector.app.core.extensions.hideKeyboard
|
||||||
import im.vector.app.core.extensions.hidePassword
|
import im.vector.app.core.extensions.hidePassword
|
||||||
|
import im.vector.app.core.extensions.isMatrixId
|
||||||
|
import im.vector.app.core.extensions.onTextChange
|
||||||
import im.vector.app.core.extensions.realignPercentagesToParent
|
import im.vector.app.core.extensions.realignPercentagesToParent
|
||||||
import im.vector.app.core.extensions.setOnFocusLostListener
|
import im.vector.app.core.extensions.setOnFocusLostListener
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
@ -46,6 +49,7 @@ import im.vector.app.features.onboarding.OnboardingAction
|
|||||||
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
import im.vector.app.features.onboarding.OnboardingAction.AuthenticateAction
|
||||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||||
import im.vector.app.features.onboarding.OnboardingViewState
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||||
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
import org.matrix.android.sdk.api.failure.isHomeserverUnavailable
|
||||||
@ -55,8 +59,11 @@ import org.matrix.android.sdk.api.failure.isLoginEmailUnknown
|
|||||||
import org.matrix.android.sdk.api.failure.isRegistrationDisabled
|
import org.matrix.android.sdk.api.failure.isRegistrationDisabled
|
||||||
import org.matrix.android.sdk.api.failure.isUsernameInUse
|
import org.matrix.android.sdk.api.failure.isUsernameInUse
|
||||||
import org.matrix.android.sdk.api.failure.isWeakPassword
|
import org.matrix.android.sdk.api.failure.isWeakPassword
|
||||||
|
import reactivecircus.flowbinding.android.widget.textChanges
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val MINIMUM_PASSWORD_LENGTH = 8
|
||||||
|
|
||||||
class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentFtueCombinedRegisterBinding>() {
|
class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentFtueCombinedRegisterBinding>() {
|
||||||
|
|
||||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding {
|
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding {
|
||||||
@ -69,15 +76,27 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||||||
views.createAccountRoot.realignPercentagesToParent()
|
views.createAccountRoot.realignPercentagesToParent()
|
||||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||||
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||||
|
|
||||||
|
views.createAccountInput.onTextChange(viewLifecycleOwner) {
|
||||||
|
viewModel.handle(OnboardingAction.ResetSelectedRegistrationUserName)
|
||||||
|
views.createAccountEntryFooter.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
views.createAccountInput.setOnFocusLostListener {
|
views.createAccountInput.setOnFocusLostListener {
|
||||||
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content()))
|
viewModel.handle(OnboardingAction.UserNameEnteredAction.Registration(views.createAccountInput.content()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupSubmitButton() {
|
private fun setupSubmitButton() {
|
||||||
views.createAccountSubmit.setOnClickListener { submit() }
|
views.createAccountSubmit.setOnClickListener { submit() }
|
||||||
observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit)
|
views.createAccountInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
views.createAccountPasswordInput.clearErrorOnChange(viewLifecycleOwner)
|
||||||
|
|
||||||
|
combine(views.createAccountInput.editText().textChanges(), views.createAccountPasswordInput.editText().textChanges()) { account, password ->
|
||||||
|
val accountIsValid = account.isNotEmpty()
|
||||||
|
val passwordIsValid = password.length >= MINIMUM_PASSWORD_LENGTH
|
||||||
|
views.createAccountSubmit.isEnabled = accountIsValid && passwordIsValid
|
||||||
|
}.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun submit() {
|
private fun submit() {
|
||||||
@ -103,7 +122,12 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error == 0) {
|
if (error == 0) {
|
||||||
viewModel.handle(AuthenticateAction.Register(login, password, getString(R.string.login_default_session_public_name)))
|
val initialDeviceName = getString(R.string.login_default_session_public_name)
|
||||||
|
val registerAction = when {
|
||||||
|
login.isMatrixId() -> AuthenticateAction.RegisterWithMatrixId(login, password, initialDeviceName)
|
||||||
|
else -> AuthenticateAction.Register(login, password, initialDeviceName)
|
||||||
|
}
|
||||||
|
viewModel.handle(registerAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -153,17 +177,25 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
|||||||
override fun updateWithState(state: OnboardingViewState) {
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
setupUi(state)
|
setupUi(state)
|
||||||
setupAutoFill()
|
setupAutoFill()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setupUi(state: OnboardingViewState) {
|
||||||
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||||
views.selectedServerDescription.text = state.selectedHomeserver.description
|
|
||||||
|
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
// Ensure password is hidden
|
// Ensure password is hidden
|
||||||
views.createAccountPasswordInput.editText().hidePassword()
|
views.createAccountPasswordInput.editText().hidePassword()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
views.createAccountEntryFooter.text = when {
|
||||||
|
state.registrationState.isUserNameAvailable -> getString(
|
||||||
|
R.string.ftue_auth_create_account_username_entry_footer,
|
||||||
|
state.registrationState.selectedMatrixId
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> ""
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupUi(state: OnboardingViewState) {
|
|
||||||
when (state.selectedHomeserver.preferredLoginMode) {
|
when (state.selectedHomeserver.preferredLoginMode) {
|
||||||
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||||
else -> hideSsoProviders()
|
else -> hideSsoProviders()
|
||||||
|
@ -20,14 +20,17 @@ import android.os.Bundle
|
|||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.associateContentStateWith
|
import im.vector.app.core.extensions.associateContentStateWith
|
||||||
import im.vector.app.core.extensions.autofillEmail
|
import im.vector.app.core.extensions.autofillEmail
|
||||||
import im.vector.app.core.extensions.clearErrorOnChange
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtueEmailInputBinding
|
import im.vector.app.databinding.FragmentFtueEmailInputBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import im.vector.app.features.onboarding.RegisterAction
|
import im.vector.app.features.onboarding.RegisterAction
|
||||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -56,6 +59,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
|
|||||||
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(email))))
|
viewModel.handle(OnboardingAction.PostRegisterAction(RegisterAction.AddThreePid(RegisterThreePid.Email(email))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
views.emailEntryHeaderSubtitle.text = getString(R.string.ftue_auth_email_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(throwable: Throwable) {
|
override fun onError(throwable: Throwable) {
|
||||||
views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,10 @@ import im.vector.app.core.extensions.autofillPhoneNumber
|
|||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.editText
|
import im.vector.app.core.extensions.editText
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtuePhoneInputBinding
|
import im.vector.app.databinding.FragmentFtuePhoneInputBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
import im.vector.app.features.onboarding.RegisterAction
|
import im.vector.app.features.onboarding.RegisterAction
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
@ -77,6 +79,10 @@ class FtueAuthPhoneEntryFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
views.phoneEntryHeaderSubtitle.text = getString(R.string.ftue_auth_phone_subtitle, state.selectedHomeserver.userFacingUrl.toReducedUrl())
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(throwable: Throwable) {
|
override fun onError(throwable: Throwable) {
|
||||||
views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ class FtueAuthResetPasswordBreakerFragment : AbstractFtueAuthFragment<FragmentFt
|
|||||||
views.resetPasswordBreakerGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground())
|
views.resetPasswordBreakerGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground())
|
||||||
views.resetPasswordBreakerTitle.text = getString(R.string.ftue_auth_reset_password_breaker_title)
|
views.resetPasswordBreakerTitle.text = getString(R.string.ftue_auth_reset_password_breaker_title)
|
||||||
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
|
.colorTerminatingFullStop(ThemeUtils.getColor(requireContext(), R.attr.colorSecondary))
|
||||||
views.resetPasswordBreakerSubtitle.text = getString(R.string.ftue_auth_email_verification_subtitle, params.email)
|
views.resetPasswordBreakerSubtitle.text = getString(R.string.ftue_auth_password_reset_email_confirmation_subtitle, params.email)
|
||||||
views.resetPasswordBreakerResendEmail.debouncedClicks { viewModel.handle(OnboardingAction.ResendResetPassword) }
|
views.resetPasswordBreakerResendEmail.debouncedClicks { viewModel.handle(OnboardingAction.ResendResetPassword) }
|
||||||
views.resetPasswordBreakerFooter.debouncedClicks {
|
views.resetPasswordBreakerFooter.debouncedClicks {
|
||||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordBreakerConfirmed))
|
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordBreakerConfirmed))
|
||||||
|
@ -21,13 +21,16 @@ import android.view.LayoutInflater
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.associateContentStateWith
|
import im.vector.app.core.extensions.associateContentStateWith
|
||||||
import im.vector.app.core.extensions.clearErrorOnChange
|
import im.vector.app.core.extensions.clearErrorOnChange
|
||||||
import im.vector.app.core.extensions.content
|
import im.vector.app.core.extensions.content
|
||||||
import im.vector.app.core.extensions.isEmail
|
import im.vector.app.core.extensions.isEmail
|
||||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||||
|
import im.vector.app.core.extensions.toReducedUrl
|
||||||
import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding
|
import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
|
import im.vector.app.features.onboarding.OnboardingViewState
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment<FragmentFtueResetPasswordEmailInputBinding>() {
|
class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment<FragmentFtueResetPasswordEmailInputBinding>() {
|
||||||
@ -53,6 +56,13 @@ class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment<Fragmen
|
|||||||
viewModel.handle(OnboardingAction.ResetPassword(email = email, newPassword = null))
|
viewModel.handle(OnboardingAction.ResetPassword(email = email, newPassword = null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
|
views.emailEntryHeaderSubtitle.text = getString(
|
||||||
|
R.string.ftue_auth_reset_password_email_subtitle,
|
||||||
|
state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onError(throwable: Throwable) {
|
override fun onError(throwable: Throwable) {
|
||||||
views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||||
}
|
}
|
||||||
|
@ -16,16 +16,10 @@
|
|||||||
|
|
||||||
package im.vector.app.features.onboarding.ftueauth
|
package im.vector.app.features.onboarding.ftueauth
|
||||||
|
|
||||||
import android.widget.Button
|
|
||||||
import com.google.android.material.textfield.TextInputLayout
|
|
||||||
import im.vector.app.R
|
import im.vector.app.R
|
||||||
import im.vector.app.core.extensions.hasContentFlow
|
|
||||||
import im.vector.app.features.login.SignMode
|
import im.vector.app.features.login.SignMode
|
||||||
import im.vector.app.features.onboarding.OnboardingAction
|
import im.vector.app.features.onboarding.OnboardingAction
|
||||||
import im.vector.app.features.themes.ThemeProvider
|
import im.vector.app.features.themes.ThemeProvider
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
|
|
||||||
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
|
fun SignMode.toAuthenticateAction(login: String, password: String, initialDeviceName: String): OnboardingAction.AuthenticateAction {
|
||||||
return when (this) {
|
return when (this) {
|
||||||
@ -36,22 +30,6 @@ fun SignMode.toAuthenticateAction(login: String, password: String, initialDevice
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A flow to monitor content changes from both username/id and password fields,
|
|
||||||
* clearing errors and enabling/disabling the submission button on non empty content changes.
|
|
||||||
*/
|
|
||||||
fun observeContentChangesAndResetErrors(username: TextInputLayout, password: TextInputLayout, submit: Button): Flow<*> {
|
|
||||||
return combine(
|
|
||||||
username.hasContentFlow { it.trim() },
|
|
||||||
password.hasContentFlow(),
|
|
||||||
transform = { usernameHasContent, passwordHasContent -> usernameHasContent && passwordHasContent }
|
|
||||||
).onEach {
|
|
||||||
username.error = null
|
|
||||||
password.error = null
|
|
||||||
submit.isEnabled = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) {
|
fun ThemeProvider.ftueBreakerBackground() = when (isLightTheme()) {
|
||||||
true -> R.drawable.bg_gradient_ftue_breaker
|
true -> R.drawable.bg_gradient_ftue_breaker
|
||||||
false -> R.drawable.bg_color_background
|
false -> R.drawable.bg_color_background
|
||||||
|
@ -114,7 +114,9 @@ class FtueAuthTermsFragment @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun updateWithState(state: OnboardingViewState) {
|
override fun updateWithState(state: OnboardingViewState) {
|
||||||
policyController.homeServer = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
val homeserverName = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||||
|
views.termsHeaderSubtitle.text = getString(R.string.ftue_auth_terms_subtitle, homeserverName)
|
||||||
|
policyController.homeServer = homeserverName
|
||||||
renderState()
|
renderState()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,26 +73,30 @@ class BiometricHelper @Inject constructor(
|
|||||||
/**
|
/**
|
||||||
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
|
* Returns true if a weak biometric method (i.e.: some face or iris unlock implementations) can be used.
|
||||||
*/
|
*/
|
||||||
val canUseWeakBiometricAuth: Boolean get() =
|
val canUseWeakBiometricAuth: Boolean
|
||||||
|
get() =
|
||||||
configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
|
configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
|
* Returns true if a strong biometric method (i.e.: fingerprint, some face or iris unlock implementations) can be used.
|
||||||
*/
|
*/
|
||||||
val canUseStrongBiometricAuth: Boolean get() =
|
val canUseStrongBiometricAuth: Boolean
|
||||||
|
get() =
|
||||||
configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
|
configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.).
|
* Returns true if the device credentials can be used to unlock (system pin code, password, pattern, etc.).
|
||||||
*/
|
*/
|
||||||
val canUseDeviceCredentialsAuth: Boolean get() =
|
val canUseDeviceCredentialsAuth: Boolean
|
||||||
|
get() =
|
||||||
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
|
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
|
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
|
||||||
*/
|
*/
|
||||||
@VisibleForTesting(otherwise = PRIVATE)
|
@VisibleForTesting(otherwise = PRIVATE)
|
||||||
internal val canUseAnySystemAuth: Boolean get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth
|
internal val canUseAnySystemAuth: Boolean
|
||||||
|
get() = canUseWeakBiometricAuth || canUseStrongBiometricAuth || canUseDeviceCredentialsAuth
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if any system authentication method and there is a valid associated key.
|
* Returns true if any system authentication method and there is a valid associated key.
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* 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.redaction
|
||||||
|
|
||||||
|
import org.matrix.android.sdk.api.session.Session
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class CheckIfEventIsRedactedUseCase @Inject constructor(
|
||||||
|
private val session: Session,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun execute(roomId: String, eventId: String): Boolean {
|
||||||
|
Timber.d("checking if event is redacted for roomId=$roomId and eventId=$eventId")
|
||||||
|
return try {
|
||||||
|
session.eventService()
|
||||||
|
.getEvent(roomId, eventId)
|
||||||
|
.isRedacted()
|
||||||
|
.also { Timber.d("event isRedacted=$it") }
|
||||||
|
} catch (error: Exception) {
|
||||||
|
Timber.e(error, "error when getting event, it may not exist yet")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -110,6 +110,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
?.filter {
|
?.filter {
|
||||||
it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId)
|
it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId)
|
||||||
}
|
}
|
||||||
|
?.filterNot { it.isUpgradedRoom(data) }
|
||||||
?: emptyList()
|
?: emptyList()
|
||||||
|
|
||||||
if (flattenChildInfo.isEmpty()) {
|
if (flattenChildInfo.isEmpty()) {
|
||||||
@ -209,4 +210,7 @@ class SpaceDirectoryController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun SpaceChildInfo.isUpgradedRoom(data: SpaceDirectoryState) =
|
||||||
|
data.knownRoomSummaries.any { it.roomId == childRoomId && it.versioningState.isUpgraded() }
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="8dp" />
|
||||||
|
<solid android:color="?android:colorBackground" />
|
||||||
|
</shape>
|
@ -140,7 +140,7 @@
|
|||||||
style="@style/Widget.Vector.TextInputLayout.Username"
|
style="@style/Widget.Vector.TextInputLayout.Username"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/username"
|
android:hint="@string/ftue_auth_login_username_entry"
|
||||||
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
|
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/loginGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
app:layout_constraintStart_toStartOf="@id/loginGutterStart"
|
||||||
|
@ -34,8 +34,8 @@
|
|||||||
android:layout_height="52dp"
|
android:layout_height="52dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/createAccountHeaderIcon"
|
app:layout_constraintBottom_toTopOf="@id/createAccountHeaderIcon"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_constraintVertical_bias="0"
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
app:layout_constraintVertical_chainStyle="packed" />
|
app:layout_constraintVertical_bias="0" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/createAccountHeaderIcon"
|
android:id="@+id/createAccountHeaderIcon"
|
||||||
@ -62,24 +62,10 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_create_account_title"
|
android:text="@string/ftue_auth_create_account_title"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/createAccountHeaderSubtitle"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderIcon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/createAccountHeaderSubtitle"
|
|
||||||
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_auth_create_account_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderTitle" />
|
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderIcon" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/titleContentSpacing"
|
android:id="@+id/titleContentSpacing"
|
||||||
@ -87,7 +73,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/chooseYourServerHeader"
|
app:layout_constraintBottom_toTopOf="@id/chooseYourServerHeader"
|
||||||
app:layout_constraintHeight_percent="0.03"
|
app:layout_constraintHeight_percent="0.03"
|
||||||
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderSubtitle" />
|
app:layout_constraintTop_toBottomOf="@id/createAccountHeaderTitle" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/chooseYourServerHeader"
|
android:id="@+id/chooseYourServerHeader"
|
||||||
@ -110,22 +96,11 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="12dp"
|
android:layout_marginStart="12dp"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/selectedServerDescription"
|
|
||||||
app:layout_constraintEnd_toStartOf="@id/editServerButton"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/selectedServerDescription"
|
|
||||||
style="@style/Widget.Vector.TextView.Micro"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="12dp"
|
|
||||||
android:textColor="?vctr_content_tertiary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
|
app:layout_constraintBottom_toTopOf="@id/serverSelectionSpacing"
|
||||||
app:layout_constraintEnd_toStartOf="@id/editServerButton"
|
app:layout_constraintEnd_toStartOf="@id/editServerButton"
|
||||||
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/selectedServerName" />
|
app:layout_constraintTop_toBottomOf="@id/chooseYourServerHeader"
|
||||||
|
tools:text="matrix.org" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/editServerButton"
|
android:id="@+id/editServerButton"
|
||||||
@ -137,7 +112,7 @@
|
|||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:text="@string/ftue_auth_create_account_edit_server_selection"
|
android:text="@string/ftue_auth_create_account_edit_server_selection"
|
||||||
android:textAllCaps="true"
|
android:textAllCaps="true"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/selectedServerDescription"
|
app:layout_constraintBottom_toBottomOf="@id/selectedServerName"
|
||||||
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
||||||
app:layout_constraintTop_toTopOf="@id/chooseYourServerHeader" />
|
app:layout_constraintTop_toTopOf="@id/chooseYourServerHeader" />
|
||||||
|
|
||||||
@ -147,7 +122,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/createAccountInput"
|
app:layout_constraintBottom_toTopOf="@id/createAccountInput"
|
||||||
app:layout_constraintHeight_percent="0.05"
|
app:layout_constraintHeight_percent="0.05"
|
||||||
app:layout_constraintTop_toBottomOf="@id/selectedServerDescription" />
|
app:layout_constraintTop_toBottomOf="@id/selectedServerName" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@ -185,18 +160,18 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:text="@string/ftue_auth_create_account_username_entry_footer"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
|
app:layout_constraintBottom_toTopOf="@id/entrySpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/createAccountGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
app:layout_constraintStart_toStartOf="@id/createAccountGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/createAccountInput" />
|
app:layout_constraintTop_toBottomOf="@id/createAccountInput"
|
||||||
|
tools:text="Others can discover you %s" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/entrySpacing"
|
android:id="@+id/entrySpacing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/createAccountPasswordInput"
|
app:layout_constraintBottom_toTopOf="@id/createAccountPasswordInput"
|
||||||
app:layout_constraintHeight_percent="0.03"
|
app:layout_constraintHeight_percent="0.02"
|
||||||
app:layout_constraintTop_toBottomOf="@id/createAccountEntryFooter" />
|
app:layout_constraintTop_toBottomOf="@id/createAccountEntryFooter" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
@ -58,24 +58,10 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_display_name_title"
|
android:text="@string/ftue_display_name_title"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/displayNameHeaderSubtitle"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/displayNameHeaderIcon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/displayNameHeaderSubtitle"
|
|
||||||
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_display_name_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/displayNameGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
|
app:layout_constraintStart_toStartOf="@id/displayNameGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/displayNameHeaderTitle" />
|
app:layout_constraintTop_toBottomOf="@id/displayNameHeaderIcon" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/titleContentSpacing"
|
android:id="@+id/titleContentSpacing"
|
||||||
@ -83,7 +69,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/displayNameInput"
|
app:layout_constraintBottom_toTopOf="@id/displayNameInput"
|
||||||
app:layout_constraintHeight_percent="0.03"
|
app:layout_constraintHeight_percent="0.03"
|
||||||
app:layout_constraintTop_toBottomOf="@id/displayNameHeaderSubtitle" />
|
app:layout_constraintTop_toBottomOf="@id/displayNameHeaderTitle" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
android:id="@+id/displayNameInput"
|
android:id="@+id/displayNameInput"
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_email_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
|
||||||
|
@ -53,26 +53,12 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="16dp"
|
android:layout_marginTop="16dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_create_account_title"
|
android:text="@string/ftue_auth_captcha_title"
|
||||||
android:textColor="?vctr_content_primary"
|
android:textColor="?vctr_content_primary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/captchaHeaderSubtitle"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/captchaHeaderIcon" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/captchaHeaderSubtitle"
|
|
||||||
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/auth_recaptcha_message"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/captchaGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
|
app:layout_constraintStart_toStartOf="@id/captchaGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/captchaHeaderTitle" />
|
app:layout_constraintTop_toBottomOf="@id/captchaHeaderIcon" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/titleContentSpacing"
|
android:id="@+id/titleContentSpacing"
|
||||||
@ -80,7 +66,7 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/loginCaptchaWevView"
|
app:layout_constraintBottom_toTopOf="@id/loginCaptchaWevView"
|
||||||
app:layout_constraintHeight_percent="0.03"
|
app:layout_constraintHeight_percent="0.03"
|
||||||
app:layout_constraintTop_toBottomOf="@id/captchaHeaderSubtitle" />
|
app:layout_constraintTop_toBottomOf="@id/captchaHeaderTitle" />
|
||||||
|
|
||||||
<WebView
|
<WebView
|
||||||
android:id="@+id/loginCaptchaWevView"
|
android:id="@+id/loginCaptchaWevView"
|
||||||
|
@ -73,7 +73,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_terms_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/termsGutterEnd"
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_phone_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/phoneEntryGutterEnd"
|
||||||
|
@ -70,7 +70,6 @@
|
|||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="8dp"
|
android:layout_marginTop="8dp"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:text="@string/ftue_auth_reset_password_email_subtitle"
|
|
||||||
android:textColor="?vctr_content_secondary"
|
android:textColor="?vctr_content_secondary"
|
||||||
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
app:layout_constraintBottom_toTopOf="@id/titleContentSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/emailEntryGutterEnd"
|
||||||
|
@ -97,7 +97,7 @@
|
|||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/ftue_auth_choose_server_entry_hint"
|
android:hint="@string/ftue_auth_choose_server_entry_hint"
|
||||||
app:layout_constraintBottom_toTopOf="@id/chooseServerEntryFooter"
|
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
|
||||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
||||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
||||||
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
|
app:layout_constraintTop_toBottomOf="@id/titleContentSpacing">
|
||||||
@ -111,25 +111,13 @@
|
|||||||
|
|
||||||
</com.google.android.material.textfield.TextInputLayout>
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/chooseServerEntryFooter"
|
|
||||||
style="@style/Widget.Vector.TextView.Micro"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:text="@string/ftue_auth_choose_server_entry_footer"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/actionSpacing"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/chooseServerGutterEnd"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/chooseServerGutterStart"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/chooseServerInput" />
|
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:id="@+id/actionSpacing"
|
android:id="@+id/actionSpacing"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/chooseServerSubmit"
|
app:layout_constraintBottom_toTopOf="@id/chooseServerSubmit"
|
||||||
app:layout_constraintHeight_percent="0.02"
|
app:layout_constraintHeight_percent="0.03"
|
||||||
app:layout_constraintTop_toBottomOf="@id/chooseServerEntryFooter" />
|
app:layout_constraintTop_toBottomOf="@id/chooseServerInput" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/chooseServerSubmit"
|
android:id="@+id/chooseServerSubmit"
|
||||||
|
@ -6,6 +6,11 @@
|
|||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@drawable/bg_live_location_users_bottom_sheet">
|
android:background="@drawable/bg_live_location_users_bottom_sheet">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/liveLocationPopupAnchor"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/liveLocationMapFragmentContainer"
|
android:id="@+id/liveLocationMapFragmentContainer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
<?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="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@drawable/bg_live_location_marker_popup"
|
||||||
|
android:elevation="8dp"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/shareLocationImageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
android:src="@drawable/ic_share_external"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:tint="?vctr_content_tertiary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
style="@style/TextAppearance.Vector.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
android:text="@string/live_location_share_location_item_share"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/shareLocationImageView"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -10,57 +10,5 @@
|
|||||||
<string name="cut_the_slack_from_teams" translatable="false">Cut the slack from teams.</string>
|
<string name="cut_the_slack_from_teams" translatable="false">Cut the slack from teams.</string>
|
||||||
|
|
||||||
<!-- WIP -->
|
<!-- WIP -->
|
||||||
<string name="ftue_auth_create_account_title">Create your account</string>
|
|
||||||
<string name="ftue_auth_create_account_subtitle">We\'ll need some info to get you set up.</string>
|
|
||||||
<string name="ftue_auth_create_account_username_entry_footer">You can\'t change this later</string>
|
|
||||||
<string name="ftue_auth_create_account_password_entry_footer">Must be 8 characters or more</string>
|
|
||||||
<string name="ftue_auth_create_account_choose_server_header">Choose your server to store your data</string>
|
|
||||||
<string name="ftue_auth_create_account_sso_section_header">Or</string>
|
|
||||||
<string name="ftue_auth_create_account_matrix_dot_org_server_description">Join millions for free on the largest public server</string>
|
|
||||||
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_welcome_back_title">Welcome back!</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_choose_server_title">Choose your server</string>
|
|
||||||
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? Server is like a home for all your data.</string>
|
|
||||||
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>
|
|
||||||
<string name="ftue_auth_choose_server_entry_footer">You can only connect to a server that has already been set up</string>
|
|
||||||
<string name="ftue_auth_choose_server_ems_title">Want to host your own server?</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure and real time communication. Find out how on <a href="${ftue_ems_url}">element.io/ems</a></string>
|
|
||||||
<string name="ftue_auth_choose_server_ems_cta">Get in touch</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_terms_title">Privacy policy</string>
|
|
||||||
<string name="ftue_auth_terms_subtitle">Please read through T&C. You must accept in order to continue.</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_email_title">Enter your email address</string>
|
|
||||||
<string name="ftue_auth_email_subtitle">This will help verify your account and enables password recovery.</string>
|
|
||||||
<string name="ftue_auth_email_entry_title">Email Address</string>
|
|
||||||
<string name="ftue_auth_phone_title">Enter your phone number</string>
|
|
||||||
<string name="ftue_auth_phone_subtitle">This will help verify your account and enables password recovery.</string>
|
|
||||||
<string name="ftue_auth_phone_entry_title">Phone Number</string>
|
|
||||||
<string name="ftue_auth_phone_confirmation_entry_title">Confirmation code</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_reset_password_email_subtitle">We will send you a verification link.</string>
|
|
||||||
<string name="ftue_auth_reset_password_breaker_title">Check your email.</string>
|
|
||||||
<string name="ftue_auth_new_password_entry_title">New Password</string>
|
|
||||||
<string name="ftue_auth_new_password_title">Choose a new password</string>
|
|
||||||
<string name="ftue_auth_new_password_subtitle">Make sure it\'s 8 characters or more.</string>
|
|
||||||
<string name="ftue_auth_reset_password">Reset password</string>
|
|
||||||
<string name="ftue_auth_sign_out_all_devices">Sign out all devices</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_phone_confirmation_title">Confirm your phone number</string>
|
|
||||||
<!-- Note for translators, %s is the users international phone number -->
|
|
||||||
<string name="ftue_auth_phone_confirmation_subtitle">We just sent a code to %s. Enter it below to verify it\'s you.</string>
|
|
||||||
<string name="ftue_auth_phone_confirmation_resend_code">Resend code</string>
|
|
||||||
|
|
||||||
<string name="ftue_auth_email_verification_title">Check your email to verify.</string>
|
|
||||||
<!-- Note for translators, %s is the users email address -->
|
|
||||||
<string name="ftue_auth_email_verification_subtitle">To confirm your email address, tap the button in the email we just sent to %s</string>
|
|
||||||
<string name="ftue_auth_email_verification_footer">Did not receive an email?</string>
|
|
||||||
<string name="ftue_auth_email_resend_email">Resend email</string>
|
|
||||||
<string name="ftue_auth_forgot_password">Forgot password</string>
|
|
||||||
<string name="ftue_auth_password_reset_confirmation">Password reset</string>
|
|
||||||
|
|
||||||
<string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string>
|
<string name="location_map_view_copyright" translatable="false">© MapTiler © OpenStreetMap contributors</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -1608,7 +1608,7 @@
|
|||||||
<string name="message_view_reaction">View Reactions</string>
|
<string name="message_view_reaction">View Reactions</string>
|
||||||
<string name="reactions">Reactions</string>
|
<string name="reactions">Reactions</string>
|
||||||
|
|
||||||
<string name="event_redacted">Message deleted</string>
|
<string name="event_redacted">Message removed</string>
|
||||||
<string name="settings_show_redacted">Show removed messages</string>
|
<string name="settings_show_redacted">Show removed messages</string>
|
||||||
<string name="settings_show_redacted_summary">Show a placeholder for removed messages</string>
|
<string name="settings_show_redacted_summary">Show a placeholder for removed messages</string>
|
||||||
<string name="event_redacted_by_user_reason">Event deleted by user</string>
|
<string name="event_redacted_by_user_reason">Event deleted by user</string>
|
||||||
@ -1903,31 +1903,93 @@
|
|||||||
<string name="ftue_auth_carousel_workplace_body">${app_name} is also great for the workplace. It’s trusted by the world’s most secure organisations.</string>
|
<string name="ftue_auth_carousel_workplace_body">${app_name} is also great for the workplace. It’s trusted by the world’s most secure organisations.</string>
|
||||||
|
|
||||||
<string name="ftue_auth_use_case_title">Who will you chat to the most?</string>
|
<string name="ftue_auth_use_case_title">Who will you chat to the most?</string>
|
||||||
<string name="ftue_auth_use_case_subtitle">We\'ll help you get connected.</string>
|
<string name="ftue_auth_use_case_subtitle">We\'ll help you get connected</string>
|
||||||
<string name="ftue_auth_use_case_option_one">Friends and family</string>
|
<string name="ftue_auth_use_case_option_one">Friends and family</string>
|
||||||
<string name="ftue_auth_use_case_option_two">Teams</string>
|
<string name="ftue_auth_use_case_option_two">Teams</string>
|
||||||
<string name="ftue_auth_use_case_option_three">Communities</string>
|
<string name="ftue_auth_use_case_option_three">Communities</string>
|
||||||
<!-- Note to translators: the %s is replaced by the content of ftue_auth_use_case_skip_partial -->
|
<!-- Note to translators: the %s is replaced by the content of ftue_auth_use_case_skip_partial -->
|
||||||
<string name="ftue_auth_use_case_skip">Not sure yet? You can %s</string>
|
<string name="ftue_auth_use_case_skip">Not sure yet? %s</string>
|
||||||
<string name="ftue_auth_use_case_skip_partial">skip this question</string>
|
<string name="ftue_auth_use_case_skip_partial">Skip this question</string>
|
||||||
<string name="ftue_auth_use_case_join_existing_server">Looking to join an existing server?</string>
|
<string name="ftue_auth_use_case_join_existing_server">Looking to join an existing server?</string>
|
||||||
<string name="ftue_auth_use_case_connect_to_server">Connect to server</string>
|
<string name="ftue_auth_use_case_connect_to_server">Connect to server</string>
|
||||||
|
|
||||||
<string name="ftue_account_created_personalize">Personalize profile</string>
|
<string name="ftue_account_created_personalize">Personalize profile</string>
|
||||||
<string name="ftue_account_created_take_me_home">Take me home</string>
|
<string name="ftue_account_created_take_me_home">Take me home</string>
|
||||||
<string name="ftue_account_created_congratulations_title">Congratulations!</string>
|
<string name="ftue_account_created_congratulations_title">Congratulations!</string>
|
||||||
<string name="ftue_account_created_subtitle">Your account %s has been created.</string>
|
<string name="ftue_account_created_subtitle">Your account %s has been created</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_create_account_title">Create your account</string>
|
||||||
|
<!-- Note for translators, %s is the full matrix of the account being created, eg @hello:matrix.org -->
|
||||||
|
<string name="ftue_auth_create_account_username_entry_footer">Others can discover you %s</string>
|
||||||
|
<string name="ftue_auth_create_account_password_entry_footer">Must be 8 characters or more</string>
|
||||||
|
<string name="ftue_auth_create_account_choose_server_header">Where your conversations will live</string>
|
||||||
|
<string name="ftue_auth_create_account_sso_section_header">Or</string>
|
||||||
|
<string name="ftue_auth_create_account_edit_server_selection">Edit</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_welcome_back_title">Welcome back!</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_choose_server_title">Select your server</string>
|
||||||
|
<string name="ftue_auth_choose_server_subtitle">What is the address of your server? This is like a home for all your data</string>
|
||||||
|
<string name="ftue_auth_choose_server_entry_hint">Server URL</string>
|
||||||
|
<string name="ftue_auth_choose_server_ems_title">Want to host your own server?</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_choose_server_ems_subtitle">Element Matrix Services (EMS) is a robust and reliable hosting service for fast, secure and real time communication. Find out how on <a href="${ftue_ems_url}">element.io/ems</a></string>
|
||||||
|
<string name="ftue_auth_choose_server_ems_cta">Get in touch</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_terms_title">Server policies</string>
|
||||||
|
<!-- Note for translators, %s is the homeserver name, eg matrix.org -->
|
||||||
|
<string name="ftue_auth_terms_subtitle">Please read through %s\'s terns and policies</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_email_title">Enter your email</string>
|
||||||
|
<!-- Note for translators, %s is the homeserver name, eg matrix.org -->
|
||||||
|
<string name="ftue_auth_email_subtitle">%s needs to verify your account</string>
|
||||||
|
<string name="ftue_auth_email_entry_title">Email</string>
|
||||||
|
<string name="ftue_auth_phone_title">Enter your phone number</string>
|
||||||
|
<!-- Note for translators, %s is the homeserver name, eg matrix.org -->
|
||||||
|
<string name="ftue_auth_phone_subtitle">%s needs to verify your account</string>
|
||||||
|
<string name="ftue_auth_phone_entry_title">Phone Number</string>
|
||||||
|
<string name="ftue_auth_phone_confirmation_entry_title">Confirmation code</string>
|
||||||
|
|
||||||
|
<!-- Note for translators, %s is the homeserver name, eg matrix.org -->
|
||||||
|
<string name="ftue_auth_reset_password_email_subtitle">%s will send you a verification link</string>
|
||||||
|
<string name="ftue_auth_reset_password_breaker_title">Check your email.</string>
|
||||||
|
<string name="ftue_auth_new_password_entry_title">New Password</string>
|
||||||
|
<string name="ftue_auth_new_password_title">Choose a new password</string>
|
||||||
|
<string name="ftue_auth_new_password_subtitle">Make sure it\'s 8 characters or more.</string>
|
||||||
|
<string name="ftue_auth_reset_password">Reset password</string>
|
||||||
|
<string name="ftue_auth_sign_out_all_devices">Sign out all devices</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_phone_confirmation_title">Confirm your phone number</string>
|
||||||
|
<!-- Note for translators, %s is the users international phone number -->
|
||||||
|
<string name="ftue_auth_phone_confirmation_subtitle">A code was sent to %s</string>
|
||||||
|
<string name="ftue_auth_phone_confirmation_resend_code">Resend code</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_email_verification_title">Check your email to verify.</string>
|
||||||
|
<!-- Note for translators, %s is the users email address -->
|
||||||
|
<string name="ftue_auth_email_verification_subtitle">To confirm your email, tap the button in the email we just sent to %s</string>
|
||||||
|
<string name="ftue_auth_email_verification_footer">Did not receive an email?</string>
|
||||||
|
<string name="ftue_auth_email_resend_email">Resend email</string>
|
||||||
|
<string name="ftue_auth_forgot_password">Forgot password</string>
|
||||||
|
<string name="ftue_auth_password_reset_confirmation">Password reset</string>
|
||||||
|
<!-- Note for translators, %s is the users email address -->
|
||||||
|
<string name="ftue_auth_password_reset_email_confirmation_subtitle">Follow the instructions send to %s</string>
|
||||||
|
|
||||||
|
<string name="ftue_auth_captcha_title">Are you a human?</string>
|
||||||
|
<string name="ftue_auth_login_username_entry">Username / Email / Phone</string>
|
||||||
|
|
||||||
<string name="ftue_display_name_title">Choose a display name</string>
|
<string name="ftue_display_name_title">Choose a display name</string>
|
||||||
|
|
||||||
|
<!-- TODO remove -->
|
||||||
|
<!--suppress UnusedResources -->
|
||||||
<string name="ftue_display_name_subtitle">This will be shown when you send messages.</string>
|
<string name="ftue_display_name_subtitle">This will be shown when you send messages.</string>
|
||||||
<string name="ftue_display_name_entry_title">Display Name</string>
|
<string name="ftue_display_name_entry_title">Display Name</string>
|
||||||
<string name="ftue_display_name_entry_footer">You can change this later</string>
|
<string name="ftue_display_name_entry_footer">You can change this later</string>
|
||||||
|
|
||||||
<string name="ftue_profile_picture_title">Add a profile picture</string>
|
<string name="ftue_profile_picture_title">Add a profile picture</string>
|
||||||
<string name="ftue_profile_picture_subtitle">You can change this anytime.</string>
|
<string name="ftue_profile_picture_subtitle">Time to put a face to the name</string>
|
||||||
<string name="ftue_personalize_lets_go">Let\'s go</string>
|
<string name="ftue_personalize_lets_go">Let\'s go</string>
|
||||||
<string name="ftue_personalize_complete_title">You\'re all set!</string>
|
<string name="ftue_personalize_complete_title">Looking good!</string>
|
||||||
<string name="ftue_personalize_complete_subtitle">Your preferences have been saved.</string>
|
<string name="ftue_personalize_complete_subtitle">Head to settings anytime to update your profile</string>
|
||||||
|
|
||||||
<string name="ftue_personalize_submit">Save and continue</string>
|
<string name="ftue_personalize_submit">Save and continue</string>
|
||||||
<string name="ftue_personalize_skip_this_step">Skip this step</string>
|
<string name="ftue_personalize_skip_this_step">Skip this step</string>
|
||||||
@ -3056,6 +3118,7 @@
|
|||||||
<string name="live_location_bottom_sheet_last_updated_at">Updated %1$s ago</string>
|
<string name="live_location_bottom_sheet_last_updated_at">Updated %1$s ago</string>
|
||||||
<string name="live_location_not_enough_permission_dialog_title">You don’t have permission to share live location</string>
|
<string name="live_location_not_enough_permission_dialog_title">You don’t have permission to share live location</string>
|
||||||
<string name="live_location_not_enough_permission_dialog_description">You need to have the right permissions in order to share live location in this room.</string>
|
<string name="live_location_not_enough_permission_dialog_description">You need to have the right permissions in order to share live location in this room.</string>
|
||||||
|
<string name="live_location_share_location_item_share">Share location</string>
|
||||||
|
|
||||||
<string name="message_bubbles">Show Message bubbles</string>
|
<string name="message_bubbles">Show Message bubbles</string>
|
||||||
|
|
||||||
@ -3105,4 +3168,8 @@
|
|||||||
<string name="live_location_labs_promotion_description">Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.</string>
|
<string name="live_location_labs_promotion_description">Please note: this is a labs feature using a temporary implementation. This means you will not be able to delete your location history, and advanced users will be able to see your location history even after you stop sharing your live location with this room.</string>
|
||||||
<string name="live_location_labs_promotion_switch_title">Enable location sharing</string>
|
<string name="live_location_labs_promotion_switch_title">Enable location sharing</string>
|
||||||
|
|
||||||
|
<plurals name="room_removed_messages">
|
||||||
|
<item quantity="one">%d message removed</item>
|
||||||
|
<item quantity="other">%d messages removed</item>
|
||||||
|
</plurals>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -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.features.home.room.detail.location
|
||||||
|
|
||||||
|
import im.vector.app.test.fakes.FakeRoom
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Test
|
||||||
|
import org.matrix.android.sdk.api.session.events.model.Event
|
||||||
|
|
||||||
|
private const val AN_EVENT_ID = "event-id"
|
||||||
|
private const val A_REASON = "reason"
|
||||||
|
|
||||||
|
class RedactLiveLocationShareEventUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeRoom = FakeRoom()
|
||||||
|
|
||||||
|
private val redactLiveLocationShareEventUseCase = RedactLiveLocationShareEventUseCase()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event with valid id when calling use case then event is redacted in the room`() = runTest {
|
||||||
|
val event = Event(eventId = AN_EVENT_ID)
|
||||||
|
fakeRoom.locationSharingService().givenRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON)
|
||||||
|
|
||||||
|
redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON)
|
||||||
|
|
||||||
|
fakeRoom.locationSharingService().verifyRedactLiveLocationShare(beaconInfoEventId = AN_EVENT_ID, reason = A_REASON)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event with empty id when calling use case then nothing is done`() = runTest {
|
||||||
|
val event = Event(eventId = "")
|
||||||
|
|
||||||
|
redactLiveLocationShareEventUseCase.execute(event = event, room = fakeRoom, reason = A_REASON)
|
||||||
|
|
||||||
|
fakeRoom.locationSharingService().verifyRedactLiveLocationShare(inverse = true, beaconInfoEventId = "", reason = A_REASON)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,115 @@
|
|||||||
|
/*
|
||||||
|
* 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.home.room.detail.timeline.action
|
||||||
|
|
||||||
|
import im.vector.app.test.fakes.FakeActiveSessionHolder
|
||||||
|
import io.mockk.mockk
|
||||||
|
import org.amshove.kluent.shouldBe
|
||||||
|
import org.junit.Test
|
||||||
|
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.room.timeline.TimelineEvent
|
||||||
|
|
||||||
|
class CheckIfCanRedactEventUseCaseTest {
|
||||||
|
|
||||||
|
private val fakeActiveSessionHolder = FakeActiveSessionHolder()
|
||||||
|
|
||||||
|
private val checkIfCanRedactEventUseCase = CheckIfCanRedactEventUseCase(
|
||||||
|
activeSessionHolder = fakeActiveSessionHolder.instance
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event which can be redacted and owned by user when use case executes then the result is true`() {
|
||||||
|
val canRedactEventTypes = listOf(EventType.MESSAGE, EventType.STICKER) +
|
||||||
|
EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
|
||||||
|
|
||||||
|
canRedactEventTypes.forEach { eventType ->
|
||||||
|
val event = givenAnEvent(
|
||||||
|
eventType = eventType,
|
||||||
|
senderId = fakeActiveSessionHolder.fakeSession.myUserId
|
||||||
|
)
|
||||||
|
|
||||||
|
val actionPermissions = givenActionPermissions(canRedact = false)
|
||||||
|
|
||||||
|
val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||||
|
|
||||||
|
result shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given redact permission and an event which can be redacted and sent by another user when use case executes then the result is true`() {
|
||||||
|
val event = givenAnEvent(
|
||||||
|
eventType = EventType.MESSAGE,
|
||||||
|
senderId = "user-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
val actionPermissions = givenActionPermissions(canRedact = true)
|
||||||
|
|
||||||
|
val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||||
|
|
||||||
|
result shouldBe true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given an event which cannot be redacted when use case executes then the result is false`() {
|
||||||
|
val event = givenAnEvent(
|
||||||
|
eventType = EventType.CALL_ANSWER,
|
||||||
|
senderId = fakeActiveSessionHolder.fakeSession.myUserId
|
||||||
|
)
|
||||||
|
|
||||||
|
val actionPermissions = givenActionPermissions(canRedact = false)
|
||||||
|
|
||||||
|
val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||||
|
|
||||||
|
result shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given missing redact permission and an event which can be redacted and sent by another user when use case executes then the result is false`() {
|
||||||
|
val event = givenAnEvent(
|
||||||
|
eventType = EventType.MESSAGE,
|
||||||
|
senderId = "user-id"
|
||||||
|
)
|
||||||
|
|
||||||
|
val actionPermissions = givenActionPermissions(canRedact = false)
|
||||||
|
|
||||||
|
val result = checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||||
|
|
||||||
|
result shouldBe false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenAnEvent(eventType: String, senderId: String): TimelineEvent {
|
||||||
|
val eventId = "event-id"
|
||||||
|
return TimelineEvent(
|
||||||
|
root = Event(
|
||||||
|
eventId = eventId,
|
||||||
|
type = eventType,
|
||||||
|
senderId = senderId
|
||||||
|
),
|
||||||
|
localId = 123L,
|
||||||
|
eventId = eventId,
|
||||||
|
displayIndex = 1,
|
||||||
|
ownedByThreadChunk = false,
|
||||||
|
senderInfo = mockk()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun givenActionPermissions(canRedact: Boolean): ActionPermissions {
|
||||||
|
return ActionPermissions(canRedact = canRedact)
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user