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_parameters = true
|
||||
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_assignment_wrap = off
|
||||
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"
|
||||
// Ref: https://kotlinlang.org/releases.html
|
||||
def kotlin = "1.6.21"
|
||||
def kotlinCoroutines = "1.6.3"
|
||||
def kotlinCoroutines = "1.6.4"
|
||||
def dagger = "2.42"
|
||||
def retrofit = "2.9.0"
|
||||
def arrow = "0.8.2"
|
||||
|
@ -156,6 +156,20 @@ object MatrixPatterns {
|
||||
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 (~),
|
||||
* 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.
|
||||
*/
|
||||
fun getDecryptedTextSummary(): String? {
|
||||
if (isRedacted()) return "Message Deleted"
|
||||
if (isRedacted()) return "Message removed"
|
||||
val text = getDecryptedValue() ?: run {
|
||||
if (isPoll()) {
|
||||
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.isLiveLocation(): Boolean = getClearType() in EventType.STATE_ROOM_BEACON_INFO
|
||||
|
||||
fun Event.getRelationContent(): RelationDefaultContent? {
|
||||
return if (isEncrypted()) {
|
||||
content.toModel<EncryptedEventContent>()?.relatesTo
|
||||
|
@ -16,7 +16,6 @@
|
||||
|
||||
package org.matrix.android.sdk.api.session.room.location
|
||||
|
||||
import androidx.annotation.MainThread
|
||||
import androidx.lifecycle.LiveData
|
||||
import org.matrix.android.sdk.api.session.room.model.livelocation.LiveLocationShareAggregatedSummary
|
||||
import org.matrix.android.sdk.api.util.Cancelable
|
||||
@ -59,16 +58,21 @@ interface LocationSharingService {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@MainThread
|
||||
fun getRunningLiveLocationShareSummaries(): LiveData<List<LiveLocationShareAggregatedSummary>>
|
||||
|
||||
/**
|
||||
* Returns a LiveData on the live location share summary with the given eventId.
|
||||
* @param beaconInfoEventId event id of the initial beacon info state event
|
||||
*/
|
||||
@MainThread
|
||||
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.
|
||||
*/
|
||||
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.getRelationContent
|
||||
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.isReply
|
||||
import org.matrix.android.sdk.api.session.events.model.isSticker
|
||||
@ -165,6 +166,10 @@ fun TimelineEvent.isSticker(): Boolean {
|
||||
return root.isSticker()
|
||||
}
|
||||
|
||||
fun TimelineEvent.isLiveLocation(): Boolean {
|
||||
return root.isLiveLocation()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether or not the event is a root thread event.
|
||||
*/
|
||||
|
@ -37,9 +37,9 @@ import org.matrix.android.sdk.internal.auth.db.PendingSessionData
|
||||
* This class execute the registration request and is responsible to keep the session of interactive authentication.
|
||||
*/
|
||||
internal class DefaultRegistrationWizard(
|
||||
authAPI: AuthAPI,
|
||||
private val sessionCreator: SessionCreator,
|
||||
private val pendingSessionStore: PendingSessionStore
|
||||
authAPI: AuthAPI,
|
||||
private val sessionCreator: SessionCreator,
|
||||
private val pendingSessionStore: PendingSessionStore
|
||||
) : RegistrationWizard {
|
||||
|
||||
private var pendingSessionData: PendingSessionData = pendingSessionStore.getPendingSessionData() ?: error("Pending session data should exist here")
|
||||
@ -74,20 +74,20 @@ internal class DefaultRegistrationWizard(
|
||||
initialDeviceDisplayName: String?
|
||||
): RegistrationResult {
|
||||
val params = RegistrationParams(
|
||||
username = userName,
|
||||
password = password,
|
||||
initialDeviceDisplayName = initialDeviceDisplayName
|
||||
username = userName,
|
||||
password = password,
|
||||
initialDeviceDisplayName = initialDeviceDisplayName
|
||||
)
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
.also {
|
||||
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
}
|
||||
.also {
|
||||
pendingSessionData = pendingSessionData.copy(isRegistrationStarted = true)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun performReCaptcha(response: String): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams.createForCaptcha(safeSession, response))
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
@ -95,7 +95,7 @@ internal class DefaultRegistrationWizard(
|
||||
|
||||
override suspend fun acceptTerms(): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.TERMS, session = safeSession))
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
@ -103,14 +103,14 @@ internal class DefaultRegistrationWizard(
|
||||
|
||||
override suspend fun addThreePid(threePid: RegisterThreePid): RegistrationResult {
|
||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = null)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
return sendThreePid(threePid)
|
||||
}
|
||||
|
||||
override suspend fun sendAgainThreePid(): RegistrationResult {
|
||||
val safeCurrentThreePid = pendingSessionData.currentThreePidData?.threePid
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
return sendThreePid(safeCurrentThreePid)
|
||||
}
|
||||
@ -126,7 +126,7 @@ internal class DefaultRegistrationWizard(
|
||||
)
|
||||
|
||||
pendingSessionData = pendingSessionData.copy(sendAttempt = pendingSessionData.sendAttempt + 1)
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
val params = RegistrationParams(
|
||||
auth = if (threePid is RegisterThreePid.Email) {
|
||||
@ -149,7 +149,7 @@ internal class DefaultRegistrationWizard(
|
||||
)
|
||||
// Store data
|
||||
pendingSessionData = pendingSessionData.copy(currentThreePidData = ThreePidData.from(threePid, response, params))
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
.also { pendingSessionStore.savePendingSessionData(it) }
|
||||
|
||||
// and send the sid a first time
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
@ -157,7 +157,7 @@ internal class DefaultRegistrationWizard(
|
||||
|
||||
override suspend fun checkIfEmailHasBeenValidated(delayMillis: Long): RegistrationResult {
|
||||
val safeParam = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
|
||||
return performRegistrationRequest(safeParam, LoginType.PASSWORD, delayMillis)
|
||||
}
|
||||
@ -168,13 +168,13 @@ internal class DefaultRegistrationWizard(
|
||||
|
||||
private suspend fun validateThreePid(code: String): RegistrationResult {
|
||||
val registrationParams = pendingSessionData.currentThreePidData?.registrationParams
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
?: throw IllegalStateException("developer error, no pending three pid")
|
||||
val safeCurrentData = pendingSessionData.currentThreePidData ?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
val url = safeCurrentData.addThreePidRegistrationResponse.submitUrl ?: throw IllegalStateException("Missing url to send the code")
|
||||
val validationBody = ValidationCodeBody(
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
code = code
|
||||
clientSecret = pendingSessionData.clientSecret,
|
||||
sid = safeCurrentData.addThreePidRegistrationResponse.sid,
|
||||
code = code
|
||||
)
|
||||
val validationResponse = validateCodeTask.execute(ValidateCodeTask.Params(url, validationBody))
|
||||
if (validationResponse.isSuccess()) {
|
||||
@ -189,7 +189,7 @@ internal class DefaultRegistrationWizard(
|
||||
|
||||
override suspend fun dummy(): RegistrationResult {
|
||||
val safeSession = pendingSessionData.currentSession
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
?: throw IllegalStateException("developer error, call createAccount() method first")
|
||||
|
||||
val params = RegistrationParams(auth = AuthParams(type = LoginFlowTypes.DUMMY, session = safeSession))
|
||||
return performRegistrationRequest(params, LoginType.PASSWORD)
|
||||
|
@ -604,14 +604,16 @@ internal class MXOlmDevice @Inject constructor(
|
||||
* @param sharedHistory MSC3061, this key is sharable on invite
|
||||
* @return true if the operation succeeds.
|
||||
*/
|
||||
fun addInboundGroupSession(sessionId: String,
|
||||
sessionKey: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean,
|
||||
sharedHistory: Boolean): AddSessionResult {
|
||||
fun addInboundGroupSession(
|
||||
sessionId: String,
|
||||
sessionKey: String,
|
||||
roomId: String,
|
||||
senderKey: String,
|
||||
forwardingCurve25519KeyChain: List<String>,
|
||||
keysClaimed: Map<String, String>,
|
||||
exportFormat: Boolean,
|
||||
sharedHistory: Boolean
|
||||
): AddSessionResult {
|
||||
val candidateSession = tryOrNull("Failed to create inbound session in room $roomId") {
|
||||
if (exportFormat) {
|
||||
OlmInboundGroupSession.importSession(sessionKey)
|
||||
@ -701,8 +703,8 @@ internal class MXOlmDevice @Inject constructor(
|
||||
val senderKey = megolmSessionData.senderKey ?: continue
|
||||
val roomId = megolmSessionData.roomId
|
||||
|
||||
val candidateSessionToImport = try {
|
||||
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
|
||||
val candidateSessionToImport = try {
|
||||
MXInboundMegolmSessionWrapper.newFromMegolmData(megolmSessionData, true)
|
||||
} catch (e: Throwable) {
|
||||
Timber.tag(loggerTag.value).e(e, "## importInboundGroupSession() : Failed to import session $senderKey/$sessionId")
|
||||
continue
|
||||
|
@ -38,6 +38,7 @@ internal class MXMegolmDecryptionFactory @Inject constructor(
|
||||
outgoingKeyRequestManager,
|
||||
cryptoStore,
|
||||
matrixConfiguration,
|
||||
eventsManager)
|
||||
eventsManager
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -250,8 +250,10 @@ internal class MXMegolmEncryption(
|
||||
* @param sessionInfo the session info
|
||||
* @param devicesByUser the devices map
|
||||
*/
|
||||
private suspend fun shareUserDevicesKey(sessionInfo: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>) {
|
||||
private suspend fun shareUserDevicesKey(
|
||||
sessionInfo: MXOutboundSessionInfo,
|
||||
devicesByUser: Map<String, List<CryptoDeviceInfo>>
|
||||
) {
|
||||
val sessionKey = olmDevice.getSessionKey(sessionInfo.sessionId) ?: return Unit.also {
|
||||
Timber.tag(loggerTag.value).v("shareUserDevicesKey() Failed to share session, failed to export")
|
||||
}
|
||||
|
@ -40,9 +40,9 @@ import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Schema version history:
|
||||
* 0, 1, 2: legacy Riot-Android
|
||||
* 3: migrate to RiotX schema
|
||||
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6)
|
||||
* 0, 1, 2: legacy Riot-Android;
|
||||
* 3: migrate to RiotX schema;
|
||||
* 4, 5, 6, 7, 8, 9: migrations from RiotX (which was previously 1, 2, 3, 4, 5, 6).
|
||||
*/
|
||||
internal class RealmCryptoStoreMigration @Inject constructor(
|
||||
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.MigrateSessionTo031
|
||||
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.database.MatrixRealmMigration
|
||||
import javax.inject.Inject
|
||||
@ -57,7 +58,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
private val normalizer: Normalizer
|
||||
) : MatrixRealmMigration(
|
||||
dbName = "Session",
|
||||
schemaVersion = 32L,
|
||||
schemaVersion = 33L,
|
||||
) {
|
||||
/**
|
||||
* Forces all RealmSessionStoreMigration instances to be equal.
|
||||
@ -99,5 +100,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
|
||||
if (oldVersion < 30) MigrateSessionTo030(realm).perform()
|
||||
if (oldVersion < 31) MigrateSessionTo031(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
|
||||
|
||||
import io.realm.RealmList
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.PrimaryKey
|
||||
|
||||
@ -29,6 +30,11 @@ internal open class LiveLocationShareAggregatedSummaryEntity(
|
||||
@PrimaryKey
|
||||
var eventId: String = "",
|
||||
|
||||
/**
|
||||
* List of event ids used to compute the aggregated summary data.
|
||||
*/
|
||||
var relatedEventIds: RealmList<String> = RealmList(),
|
||||
|
||||
var roomId: 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.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> {
|
||||
return realm.where<EventAnnotationsSummaryEntity>()
|
||||
.equalTo(EventAnnotationsSummaryEntityFields.ROOM_ID, roomId)
|
||||
@ -44,3 +49,7 @@ internal fun EventAnnotationsSummaryEntity.Companion.getOrCreate(realm: Realm, r
|
||||
return EventAnnotationsSummaryEntity.where(realm, roomId, eventId).findFirst()
|
||||
?: 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.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(
|
||||
realm: Realm,
|
||||
roomId: String,
|
||||
@ -72,6 +80,13 @@ internal fun LiveLocationShareAggregatedSummaryEntity.Companion.get(
|
||||
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(
|
||||
realm: Realm,
|
||||
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.PollAggregationProcessor
|
||||
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.send.queue.EventSenderProcessor
|
||||
import org.matrix.android.sdk.internal.session.room.send.queue.EventSenderProcessorCoroutine
|
||||
@ -321,6 +322,10 @@ internal abstract class SessionModule {
|
||||
@IntoSet
|
||||
abstract fun bindEventRedactionProcessor(processor: RedactionEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
abstract fun bindLiveLocationShareRedactionEventProcessor(processor: LiveLocationShareRedactionEventProcessor): EventInsertLiveProcessor
|
||||
|
||||
@Binds
|
||||
@IntoSet
|
||||
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.DefaultCheckIfExistingActiveLiveTask
|
||||
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.DefaultSendStaticLocationTask
|
||||
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.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.SendStaticLocationTask
|
||||
import org.matrix.android.sdk.internal.session.room.location.StartLiveLocationShareTask
|
||||
@ -339,4 +341,7 @@ internal abstract class RoomModule {
|
||||
|
||||
@Binds
|
||||
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 io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
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.toContent
|
||||
@ -73,6 +74,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
||||
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
|
||||
val isActive = aggregatedSummary.isActive.orTrue() && isLive
|
||||
val endOfLiveTimestampMillis = content.getBestTimestampMillis()?.let { it + (content.timeout ?: 0) }
|
||||
@ -144,6 +150,11 @@ internal class LiveLocationAggregationProcessor @Inject constructor(
|
||||
roomId = roomId,
|
||||
eventId = relatedEventId
|
||||
)
|
||||
|
||||
if (!event.eventId.isNullOrEmpty()) {
|
||||
addRelatedEventId(event.eventId, aggregatedSummary)
|
||||
}
|
||||
|
||||
val updatedLocationTimestamp = content.getBestTimestampMillis() ?: 0
|
||||
val currentLocationTimestamp = ContentMapper
|
||||
.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) {
|
||||
LiveLocationShareAggregatedSummaryEntity
|
||||
.findActiveLiveInRoomForUser(
|
||||
|
@ -42,6 +42,7 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||
private val startLiveLocationShareTask: StartLiveLocationShareTask,
|
||||
private val stopLiveLocationShareTask: StopLiveLocationShareTask,
|
||||
private val checkIfExistingActiveLiveTask: CheckIfExistingActiveLiveTask,
|
||||
private val redactLiveLocationShareTask: RedactLiveLocationShareTask,
|
||||
private val liveLocationShareAggregatedSummaryMapper: LiveLocationShareAggregatedSummaryMapper,
|
||||
) : LocationSharingService {
|
||||
|
||||
@ -102,6 +103,15 @@ internal class DefaultLocationSharingService @AssistedInject constructor(
|
||||
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>> {
|
||||
return monarchy.findAllMappedWithChanges(
|
||||
{ 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) {
|
||||
EventType.ENCRYPTED,
|
||||
EventType.MESSAGE,
|
||||
in EventType.STATE_ROOM_BEACON_INFO,
|
||||
in EventType.BEACON_LOCATION_DATA,
|
||||
in EventType.POLL_START -> {
|
||||
Timber.d("REDACTION for message ${eventToPrune.eventId}")
|
||||
val unsignedData = EventMapper.map(eventToPrune).unsignedData
|
||||
|
@ -22,7 +22,7 @@ import timber.log.Timber
|
||||
/**
|
||||
* Throws in debug, only log in production.
|
||||
* As this method does not always throw, next statement should be a return.
|
||||
*/
|
||||
*/
|
||||
internal fun fatalError(message: String) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
error(message)
|
||||
|
@ -35,6 +35,23 @@ class MatrixPatternsTest {
|
||||
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 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 org.amshove.kluent.shouldBeEqualTo
|
||||
import org.amshove.kluent.shouldContain
|
||||
import org.junit.Test
|
||||
import org.matrix.android.sdk.api.session.events.model.Event
|
||||
import org.matrix.android.sdk.api.session.events.model.UnsignedData
|
||||
@ -199,9 +200,10 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
age = 123,
|
||||
replacesState = AN_EVENT_ID
|
||||
)
|
||||
val stateEventId = "state-event-id"
|
||||
val event = Event(
|
||||
senderId = A_SENDER_ID,
|
||||
eventId = "",
|
||||
eventId = stateEventId,
|
||||
unsignedData = unsignedData
|
||||
)
|
||||
val beaconInfo = MessageBeaconInfoContent(
|
||||
@ -237,6 +239,7 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
aggregatedEntity.roomId shouldBeEqualTo A_ROOM_ID
|
||||
aggregatedEntity.userId shouldBeEqualTo A_SENDER_ID
|
||||
aggregatedEntity.isActive shouldBeEqualTo false
|
||||
aggregatedEntity.relatedEventIds shouldContain stateEventId
|
||||
aggregatedEntity.endOfLiveTimestampMillis shouldBeEqualTo A_TIMESTAMP + A_TIMEOUT_MILLIS
|
||||
aggregatedEntity.lastLocationContent shouldBeEqualTo null
|
||||
previousEntities.forEach { entity ->
|
||||
@ -324,7 +327,7 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
||||
unstableTimestampMillis = A_TIMESTAMP
|
||||
)
|
||||
givenLastSummaryQueryReturns(
|
||||
val aggregatedEntity = givenLastSummaryQueryReturns(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
beaconLocationContent = lastBeaconLocationContent
|
||||
@ -340,6 +343,7 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
)
|
||||
|
||||
result shouldBeEqualTo false
|
||||
aggregatedEntity.relatedEventIds shouldContain AN_EVENT_ID
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -353,7 +357,7 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
val lastBeaconLocationContent = MessageBeaconLocationDataContent(
|
||||
unstableTimestampMillis = A_TIMESTAMP - 60_000
|
||||
)
|
||||
val entity = givenLastSummaryQueryReturns(
|
||||
val aggregatedEntity = givenLastSummaryQueryReturns(
|
||||
eventId = AN_EVENT_ID,
|
||||
roomId = A_ROOM_ID,
|
||||
beaconLocationContent = lastBeaconLocationContent
|
||||
@ -369,7 +373,8 @@ internal class LiveLocationAggregationProcessorTest {
|
||||
)
|
||||
|
||||
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?.getBestLocationInfo()?.geoUri shouldBeEqualTo A_GEO_URI
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ import androidx.lifecycle.Transformations
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkAll
|
||||
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 A_TIMEOUT = 15_000L
|
||||
private const val A_DESCRIPTION = "description"
|
||||
private const val A_REASON = "reason"
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
internal class DefaultLocationSharingServiceTest {
|
||||
@ -62,6 +65,7 @@ internal class DefaultLocationSharingServiceTest {
|
||||
private val startLiveLocationShareTask = mockk<StartLiveLocationShareTask>()
|
||||
private val stopLiveLocationShareTask = mockk<StopLiveLocationShareTask>()
|
||||
private val checkIfExistingActiveLiveTask = mockk<CheckIfExistingActiveLiveTask>()
|
||||
private val redactLiveLocationShareTask = mockk<RedactLiveLocationShareTask>()
|
||||
private val fakeLiveLocationShareAggregatedSummaryMapper = mockk<LiveLocationShareAggregatedSummaryMapper>()
|
||||
|
||||
private val defaultLocationSharingService = DefaultLocationSharingService(
|
||||
@ -72,6 +76,7 @@ internal class DefaultLocationSharingServiceTest {
|
||||
startLiveLocationShareTask = startLiveLocationShareTask,
|
||||
stopLiveLocationShareTask = stopLiveLocationShareTask,
|
||||
checkIfExistingActiveLiveTask = checkIfExistingActiveLiveTask,
|
||||
redactLiveLocationShareTask = redactLiveLocationShareTask,
|
||||
liveLocationShareAggregatedSummaryMapper = fakeLiveLocationShareAggregatedSummaryMapper
|
||||
)
|
||||
|
||||
@ -209,6 +214,20 @@ internal class DefaultLocationSharingServiceTest {
|
||||
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
|
||||
fun `livedata of live summaries is correctly computed`() {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
roomId: String,
|
||||
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(
|
||||
roomId: 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) {
|
||||
verify { instance.createLocalEcho(event) }
|
||||
}
|
||||
|
@ -18,10 +18,13 @@ package org.matrix.android.sdk.test.fakes
|
||||
|
||||
import io.mockk.MockKVerificationScope
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmModel
|
||||
import io.realm.RealmObject
|
||||
import io.realm.RealmQuery
|
||||
import io.realm.RealmResults
|
||||
import io.realm.kotlin.where
|
||||
@ -97,3 +100,10 @@ inline fun <reified T : RealmModel> RealmQuery<T>.givenIsNotNull(
|
||||
every { isNotNull(fieldName) } returns 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 {
|
||||
initWith release
|
||||
applicationIdSuffix ".nightly"
|
||||
versionNameSuffix "-nightly"
|
||||
|
||||
initWith release
|
||||
// Just override the background color of the launcher icon for the nightly build.
|
||||
resValue "color", "launcher_background", "#07007E"
|
||||
|
||||
|
@ -80,6 +80,11 @@ class DebugFeaturesStateFactory @Inject constructor(
|
||||
key = DebugFeatureKeys.startDmOnFirstMsg,
|
||||
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)
|
||||
?: vectorFeatures.shouldStartDmOnFirstMessage()
|
||||
|
||||
override fun isNewAppLayoutEnabled(): Boolean = read(DebugFeatureKeys.newAppLayoutEnabled)
|
||||
?: vectorFeatures.isNewAppLayoutEnabled()
|
||||
|
||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||
if (value == null) {
|
||||
it.remove(key)
|
||||
@ -131,4 +134,5 @@ object DebugFeatureKeys {
|
||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||
val forceUsageOfOpusEncoder = booleanPreferencesKey("force-usage-of-opus-encoder")
|
||||
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 com.google.i18n.phonenumbers.NumberParseException
|
||||
import com.google.i18n.phonenumbers.PhoneNumberUtil
|
||||
import org.matrix.android.sdk.api.MatrixPatterns
|
||||
import org.matrix.android.sdk.api.extensions.ensurePrefix
|
||||
|
||||
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.isMatrixId() = MatrixPatterns.isUserId(this.toString())
|
||||
|
||||
/**
|
||||
* 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.clearErrorOnChange(lifecycleOwner: LifecycleOwner) {
|
||||
onTextChange(lifecycleOwner) {
|
||||
error = null
|
||||
isErrorEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
fun TextInputLayout.onTextChange(lifecycleOwner: LifecycleOwner, action: (CharSequence) -> Unit) {
|
||||
editText().textChanges()
|
||||
.onEach { error = null }
|
||||
.onEach(action)
|
||||
.launchIn(lifecycleOwner.lifecycleScope)
|
||||
}
|
||||
|
||||
|
@ -36,6 +36,6 @@ class AndroidSystemSettingsProvider @Inject constructor(
|
||||
) : SystemSettingsProvider {
|
||||
|
||||
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 forceUsageOfOpusEncoder(): Boolean
|
||||
fun shouldStartDmOnFirstMessage(): Boolean
|
||||
fun isNewAppLayoutEnabled(): Boolean
|
||||
|
||||
enum class OnboardingVariant {
|
||||
LEGACY,
|
||||
@ -52,4 +53,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
||||
override fun isScreenSharingEnabled(): Boolean = true
|
||||
override fun forceUsageOfOpusEncoder(): Boolean = false
|
||||
override fun shouldStartDmOnFirstMessage(): Boolean = false
|
||||
override fun isNewAppLayoutEnabled(): Boolean = false
|
||||
}
|
||||
|
@ -21,11 +21,11 @@ import im.vector.app.features.onboarding.AuthenticationDescription
|
||||
|
||||
fun AuthenticationDescription.AuthenticationType.toAnalyticsType() = when (this) {
|
||||
AuthenticationDescription.AuthenticationType.Password -> Signup.AuthenticationType.Password
|
||||
AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple
|
||||
AuthenticationDescription.AuthenticationType.Apple -> Signup.AuthenticationType.Apple
|
||||
AuthenticationDescription.AuthenticationType.Facebook -> Signup.AuthenticationType.Facebook
|
||||
AuthenticationDescription.AuthenticationType.GitHub -> Signup.AuthenticationType.GitHub
|
||||
AuthenticationDescription.AuthenticationType.GitLab -> Signup.AuthenticationType.GitLab
|
||||
AuthenticationDescription.AuthenticationType.Google -> Signup.AuthenticationType.Google
|
||||
AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO
|
||||
AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other
|
||||
AuthenticationDescription.AuthenticationType.SSO -> Signup.AuthenticationType.SSO
|
||||
AuthenticationDescription.AuthenticationType.Other -> Signup.AuthenticationType.Other
|
||||
}
|
||||
|
@ -70,7 +70,7 @@ class AvatarRenderer @Inject constructor(
|
||||
render(
|
||||
GlideApp.with(imageView),
|
||||
matrixItem,
|
||||
DrawableImageViewTarget(imageView)
|
||||
DrawableImageViewTarget(imageView),
|
||||
)
|
||||
}
|
||||
|
||||
@ -103,7 +103,7 @@ class AvatarRenderer @Inject constructor(
|
||||
render(
|
||||
glideRequests,
|
||||
matrixItem,
|
||||
DrawableImageViewTarget(imageView)
|
||||
DrawableImageViewTarget(imageView),
|
||||
)
|
||||
}
|
||||
|
||||
@ -123,7 +123,7 @@ class AvatarRenderer @Inject constructor(
|
||||
val matrixItem = MatrixItem.UserItem(
|
||||
// Need an id starting with @
|
||||
id = "@${mappedContact.displayName}",
|
||||
displayName = mappedContact.displayName
|
||||
displayName = mappedContact.displayName,
|
||||
)
|
||||
|
||||
val placeholder = getPlaceholderDrawable(matrixItem)
|
||||
@ -140,7 +140,7 @@ class AvatarRenderer @Inject constructor(
|
||||
val matrixItem = MatrixItem.UserItem(
|
||||
// Need an id starting with @
|
||||
id = profileInfo.matrixId,
|
||||
displayName = profileInfo.displayName
|
||||
displayName = profileInfo.displayName,
|
||||
)
|
||||
|
||||
val placeholder = getPlaceholderDrawable(matrixItem)
|
||||
@ -215,7 +215,7 @@ class AvatarRenderer @Inject constructor(
|
||||
.bold()
|
||||
.endConfig()
|
||||
.buildRect(matrixItem.firstLetterOfDisplayName(), avatarColor)
|
||||
.toBitmap(width = iconSize, height = iconSize)
|
||||
.toBitmap(width = iconSize, height = iconSize),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -231,7 +231,7 @@ class AvatarRenderer @Inject constructor(
|
||||
addPlaceholder: Boolean
|
||||
) {
|
||||
val transformations = mutableListOf<Transformation<Bitmap>>(
|
||||
BlurTransformation(20, sampling)
|
||||
BlurTransformation(20, sampling),
|
||||
)
|
||||
if (colorFilter != null) {
|
||||
transformations.add(ColorFilterTransformation(colorFilter))
|
||||
|
@ -219,7 +219,7 @@ class HomeActivity :
|
||||
is HomeActivitySharedAction.ShowSpaceSettings -> showSpaceSettings(sharedAction.spaceId)
|
||||
is HomeActivitySharedAction.OpenSpaceInvite -> openSpaceInvite(sharedAction.spaceId)
|
||||
HomeActivitySharedAction.SendSpaceFeedBack -> bugReporter.openBugReportScreen(this, ReportType.SPACE_BETA_FEEDBACK)
|
||||
HomeActivitySharedAction.OnCloseSpace -> onCloseSpace()
|
||||
HomeActivitySharedAction.OnCloseSpace -> onCloseSpace()
|
||||
}
|
||||
}
|
||||
.launchIn(lifecycleScope)
|
||||
|
@ -84,6 +84,4 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
||||
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
|
||||
object StopChatEffects : RoomDetailViewEvents()
|
||||
object RoomReplacementStarted : RoomDetailViewEvents()
|
||||
|
||||
data class ChangeLocationIndicator(val isVisible: Boolean) : RoomDetailViewEvents()
|
||||
}
|
||||
|
@ -75,7 +75,8 @@ data class RoomDetailViewState(
|
||||
val switchToParentSpace: Boolean = false,
|
||||
val rootThreadEventId: String? = null,
|
||||
val threadNotificationBadgeState: ThreadNotificationBadgeState = ThreadNotificationBadgeState(),
|
||||
val typingUsers: List<SenderInfo>? = null
|
||||
val typingUsers: List<SenderInfo>? = null,
|
||||
val isSharingLiveLocation: Boolean = false,
|
||||
) : MavericksState {
|
||||
|
||||
constructor(args: TimelineArgs) : this(
|
||||
|
@ -498,7 +498,6 @@ class TimelineFragment @Inject constructor(
|
||||
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
|
||||
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
|
||||
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) {
|
||||
if (error.showInDialog) displayErrorDialog(error.throwable) else showErrorInSnackbar(error.throwable)
|
||||
}
|
||||
@ -1686,6 +1681,11 @@ class TimelineFragment @Inject constructor(
|
||||
} else if (mainState.asyncInviter.complete) {
|
||||
vectorBaseActivity.finish()
|
||||
}
|
||||
updateLiveLocationIndicator(mainState.isSharingLiveLocation)
|
||||
}
|
||||
|
||||
private fun updateLiveLocationIndicator(isSharingLiveLocation: Boolean) {
|
||||
views.locationLiveStatusIndicator.isVisible = isSharingLiveLocation
|
||||
}
|
||||
|
||||
private fun FragmentTimelineBinding.hideComposerViews() {
|
||||
@ -1706,7 +1706,7 @@ class TimelineFragment @Inject constructor(
|
||||
|
||||
private fun renderToolbar(roomSummary: RoomSummary?) {
|
||||
when {
|
||||
isLocalRoom() -> {
|
||||
isLocalRoom() -> {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isVisible = false
|
||||
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||
setupToolbar(views.roomToolbar)
|
||||
@ -1724,7 +1724,7 @@ class TimelineFragment @Inject constructor(
|
||||
}
|
||||
views.includeThreadToolbar.roomToolbarThreadTitleTextView.text = resources.getText(R.string.thread_timeline_title)
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
views.includeRoomToolbar.roomToolbarContentView.isVisible = true
|
||||
views.includeThreadToolbar.roomToolbarThreadConstraintLayout.isVisible = false
|
||||
if (roomSummary == null) {
|
||||
|
@ -48,6 +48,7 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.createdirect.DirectRoomHelper
|
||||
import im.vector.app.features.crypto.keysrequest.OutboundSessionKeySharingStrategy
|
||||
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.timeline.factory.TimelineFactory
|
||||
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.timeline.Timeline
|
||||
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.threads.ThreadNotificationBadgeState
|
||||
import org.matrix.android.sdk.api.session.threads.ThreadNotificationState
|
||||
@ -135,6 +137,7 @@ class TimelineViewModel @AssistedInject constructor(
|
||||
private val notificationDrawerManager: NotificationDrawerManager,
|
||||
private val locationSharingServiceConnection: LocationSharingServiceConnection,
|
||||
private val stopLiveLocationShareUseCase: StopLiveLocationShareUseCase,
|
||||
private val redactLiveLocationShareEventUseCase: RedactLiveLocationShareEventUseCase,
|
||||
timelineFactory: TimelineFactory,
|
||||
appStateHandler: AppStateHandler,
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState),
|
||||
@ -770,7 +773,13 @@ class TimelineViewModel @AssistedInject constructor(
|
||||
|
||||
private fun handleRedactEvent(action: RoomDetailAction.RedactAction) {
|
||||
val event = room.getTimelineEvent(action.targetEventId) ?: return
|
||||
room.sendService().redactEvent(event.root, action.reason)
|
||||
if (event.isLiveLocation()) {
|
||||
viewModelScope.launch {
|
||||
redactLiveLocationShareEventUseCase.execute(event.root, room, action.reason)
|
||||
}
|
||||
} else {
|
||||
room.sendService().redactEvent(event.root, action.reason)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleUndoReact(action: RoomDetailAction.UndoReaction) {
|
||||
@ -1294,12 +1303,12 @@ class TimelineViewModel @AssistedInject constructor(
|
||||
_viewEvents.post(RoomDetailViewEvents.OnNewTimelineEvents(eventIds))
|
||||
}
|
||||
|
||||
override fun onLocationServiceRunning() {
|
||||
_viewEvents.post(RoomDetailViewEvents.ChangeLocationIndicator(isVisible = true))
|
||||
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||
setState { copy(isSharingLiveLocation = roomId in roomIds) }
|
||||
}
|
||||
|
||||
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
|
||||
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 vectorPreferences: VectorPreferences,
|
||||
private val checkIfCanReplyEventUseCase: CheckIfCanReplyEventUseCase,
|
||||
private val checkIfCanRedactEventUseCase: CheckIfCanRedactEventUseCase,
|
||||
) : VectorViewModel<MessageActionState, MessageActionsAction, EmptyViewEvents>(initialState) {
|
||||
|
||||
private val informationData = initialState.informationData
|
||||
@ -518,12 +519,7 @@ class MessageActionsViewModel @AssistedInject constructor(
|
||||
}
|
||||
|
||||
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
|
||||
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
|
||||
return checkIfCanRedactEventUseCase.execute(event, actionPermissions)
|
||||
}
|
||||
|
||||
private fun canRetry(event: TimelineEvent, actionPermissions: ActionPermissions): Boolean {
|
||||
|
@ -63,10 +63,10 @@ class EncryptionItemFactory @Inject constructor(
|
||||
isDirect && RoomLocalEcho.isLocalEchoId(event.root.roomId.orEmpty()) -> {
|
||||
R.string.direct_room_encryption_enabled_tile_description_future
|
||||
}
|
||||
isDirect -> {
|
||||
isDirect -> {
|
||||
R.string.direct_room_encryption_enabled_tile_description
|
||||
}
|
||||
else -> {
|
||||
else -> {
|
||||
R.string.encryption_enabled_tile_description
|
||||
}
|
||||
}
|
||||
|
@ -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.room.timeline.TimelineEvent
|
||||
import org.threeten.bp.LocalDateTime
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class LiveLocationShareMessageItemFactory @Inject constructor(
|
||||
@ -135,7 +134,7 @@ class LiveLocationShareMessageItemFactory @Inject constructor(
|
||||
liveLocationShareSummaryData.lastGeoUri.orEmpty(),
|
||||
getEndOfLiveDateTime(liveLocationShareSummaryData)
|
||||
)
|
||||
}.also { viewState -> Timber.d("computed viewState: $viewState") }
|
||||
}
|
||||
}
|
||||
|
||||
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.MergedTimelineEventVisibilityStateChangedListener
|
||||
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.item.BasedMergedItem
|
||||
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.extensions.orFalse
|
||||
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.content.EncryptionEventContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
@ -53,6 +53,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper
|
||||
) {
|
||||
|
||||
private val mergeableEventTypes = listOf(EventType.STATE_ROOM_MEMBER, EventType.STATE_ROOM_SERVER_ACL)
|
||||
private val collapsedEventIds = linkedSetOf<Long>()
|
||||
private val mergeItemCollapseStates = HashMap<Long, Boolean>()
|
||||
|
||||
@ -78,19 +79,65 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
callback: TimelineEventController.Callback?,
|
||||
requestModelBuild: () -> Unit
|
||||
): BasedMergedItem<*>? {
|
||||
return if (nextEvent?.root?.getClearType() == EventType.STATE_ROOM_CREATE &&
|
||||
event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel<RoomCreateContent>()?.creator)) {
|
||||
// It's the first item before room.create
|
||||
// Collapse all room configuration events
|
||||
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
|
||||
null
|
||||
} else {
|
||||
buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
return when {
|
||||
isStartOfRoomCreationSummary(event, nextEvent) ->
|
||||
buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
isStartOfSameTypeEventsSummary(event, nextEvent, addDaySeparator) ->
|
||||
buildSameTypeEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
|
||||
isStartOfRedactedEventsSummary(event, items, currentPosition, addDaySeparator) ->
|
||||
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,
|
||||
items: List<TimelineEvent>,
|
||||
partialState: TimelineEventController.PartialState,
|
||||
@ -102,11 +149,42 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
val mergedEvents = timelineEventVisibilityHelper.prevSameTypeEvents(
|
||||
items,
|
||||
currentPosition,
|
||||
2,
|
||||
MIN_NUMBER_OF_MERGED_EVENTS,
|
||||
eventIdToHighlight,
|
||||
partialState.rootThreadEventId,
|
||||
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()) {
|
||||
null
|
||||
} else {
|
||||
@ -127,7 +205,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
)
|
||||
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
|
||||
// => handle case where paginating from mergeable events and we get more
|
||||
val previousCollapseStateKey = mergedEventIds.intersect(mergeItemCollapseStates.keys).firstOrNull()
|
||||
@ -140,12 +218,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
collapsedEventIds.removeAll(mergedEventIds)
|
||||
}
|
||||
val mergeId = mergedEventIds.joinToString(separator = "_") { it.toString() }
|
||||
val summaryTitleResId = when (event.root.getClearType()) {
|
||||
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 ->
|
||||
getSummaryTitleResId(event.root)?.let { summaryTitle ->
|
||||
val attributes = MergedSimilarEventsItem.Attributes(
|
||||
summaryTitleResId = summaryTitle,
|
||||
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(
|
||||
currentPosition: Int,
|
||||
items: List<TimelineEvent>,
|
||||
@ -191,7 +274,7 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
tmpPos--
|
||||
prevEvent = items.getOrNull(tmpPos)
|
||||
}
|
||||
return if (mergedEvents.size > 2) {
|
||||
return if (mergedEvents.size > MIN_NUMBER_OF_MERGED_EVENTS) {
|
||||
var highlighted = false
|
||||
val mergedData = ArrayList<BasedMergedItem.Data>(mergedEvents.size)
|
||||
mergedEvents.reversed()
|
||||
@ -264,4 +347,8 @@ class MergedHeaderItemFactory @Inject constructor(
|
||||
fun isCollapsed(localId: Long): Boolean {
|
||||
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.REACTION,
|
||||
in EventType.POLL_RESPONSE,
|
||||
in EventType.POLL_END,
|
||||
in EventType.BEACON_LOCATION_DATA -> noticeItemFactory.create(params)
|
||||
in EventType.POLL_END -> noticeItemFactory.create(params)
|
||||
in EventType.BEACON_LOCATION_DATA -> {
|
||||
if (event.root.isRedacted()) {
|
||||
messageItemFactory.create(params)
|
||||
} else {
|
||||
noticeItemFactory.create(params)
|
||||
}
|
||||
}
|
||||
// Calls
|
||||
EventType.CALL_INVITE,
|
||||
EventType.CALL_HANGUP,
|
||||
|
@ -51,12 +51,7 @@ object TimelineDisplayableEvents {
|
||||
EventType.STATE_ROOM_JOIN_RULES,
|
||||
EventType.KEY_VERIFICATION_DONE,
|
||||
EventType.KEY_VERIFICATION_CANCEL,
|
||||
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO
|
||||
}
|
||||
|
||||
fun TimelineEvent.canBeMerged(): Boolean {
|
||||
return root.getClearType() == EventType.STATE_ROOM_MEMBER ||
|
||||
root.getClearType() == EventType.STATE_ROOM_SERVER_ACL
|
||||
) + EventType.POLL_START + EventType.STATE_ROOM_BEACON_INFO + EventType.BEACON_LOCATION_DATA
|
||||
}
|
||||
|
||||
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.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.RelationType
|
||||
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 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 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 event id if in a thread timeline
|
||||
* @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>,
|
||||
index: Int,
|
||||
minSize: Int,
|
||||
eventIdToHighlight: String?,
|
||||
rootThreadEventId: String?,
|
||||
isFromThreadTimeline: Boolean
|
||||
isFromThreadTimeline: Boolean,
|
||||
predicateToStop: PredicateToStopSearch
|
||||
): List<TimelineEvent> {
|
||||
if (index >= timelineEvents.size - 1) {
|
||||
return emptyList()
|
||||
@ -65,13 +79,15 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
} else {
|
||||
nextSubList.subList(0, indexOfNextDay)
|
||||
}
|
||||
val indexOfFirstDifferentEventType = nextSameDayEvents.indexOfFirst { it.root.getClearType() != timelineEvent.root.getClearType() }
|
||||
val sameTypeEvents = if (indexOfFirstDifferentEventType == -1) {
|
||||
val indexOfFirstDifferentEvent = nextSameDayEvents.indexOfFirst {
|
||||
predicateToStop.shouldStopSearch(oldEvent = timelineEvent.root, newEvent = it.root)
|
||||
}
|
||||
val similarEvents = if (indexOfFirstDifferentEvent == -1) {
|
||||
nextSameDayEvents
|
||||
} else {
|
||||
nextSameDayEvents.subList(0, indexOfFirstDifferentEventType)
|
||||
nextSameDayEvents.subList(0, indexOfFirstDifferentEvent)
|
||||
}
|
||||
val filteredSameTypeEvents = sameTypeEvents.filter {
|
||||
val filteredSimilarEvents = similarEvents.filter {
|
||||
shouldShowEvent(
|
||||
timelineEvent = it,
|
||||
highlightedEventId = eventIdToHighlight,
|
||||
@ -79,14 +95,11 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
rootThreadEventId = rootThreadEventId
|
||||
)
|
||||
}
|
||||
if (filteredSameTypeEvents.size < minSize) {
|
||||
return emptyList()
|
||||
}
|
||||
return filteredSameTypeEvents
|
||||
return if (filteredSimilarEvents.size < minSize) emptyList() else filteredSimilarEvents
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 minSize the minimum number of same type events to have sequentially, otherwise will return an empty list
|
||||
* @param eventIdToHighlight used to compute visibility
|
||||
@ -107,7 +120,44 @@ class TimelineEventVisibilityHelper @Inject constructor(private val userPreferen
|
||||
return prevSub
|
||||
.reversed()
|
||||
.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
|
||||
}
|
||||
|
||||
if (root.getClearType() in EventType.BEACON_LOCATION_DATA) {
|
||||
return !root.isRedacted()
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -84,8 +84,6 @@ class UpgradeRoomViewModelTask @Inject constructor(
|
||||
// autoJoin = currentInfo.autoJoin ?: false,
|
||||
suggested = currentInfo.suggested
|
||||
)
|
||||
|
||||
parentSpace.removeChildren(params.roomId)
|
||||
}
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
|
@ -146,17 +146,17 @@ class RoomListViewModel @AssistedInject constructor(
|
||||
companion object : MavericksViewModelFactory<RoomListViewModel, RoomListViewState> by hiltMavericksViewModelFactory()
|
||||
|
||||
private val roomListSectionBuilder = RoomListSectionBuilder(
|
||||
session,
|
||||
stringProvider,
|
||||
appStateHandler,
|
||||
viewModelScope,
|
||||
autoAcceptInvites,
|
||||
{
|
||||
updatableQuery = it
|
||||
},
|
||||
suggestedRoomJoiningState,
|
||||
!vectorPreferences.prefSpacesShowAllRoomInHome()
|
||||
)
|
||||
session,
|
||||
stringProvider,
|
||||
appStateHandler,
|
||||
viewModelScope,
|
||||
autoAcceptInvites,
|
||||
{
|
||||
updatableQuery = it
|
||||
},
|
||||
suggestedRoomJoiningState,
|
||||
!vectorPreferences.prefSpacesShowAllRoomInHome()
|
||||
)
|
||||
|
||||
val sections: List<RoomsSection> by lazy {
|
||||
roomListSectionBuilder.buildSections(initialState.displayMode)
|
||||
|
@ -85,27 +85,27 @@ class EventHtmlRenderer @Inject constructor(
|
||||
} else {
|
||||
builder
|
||||
}
|
||||
.usePlugin(
|
||||
MarkwonInlineParserPlugin.create(
|
||||
/* Configuring the Markwon inline formatting processor.
|
||||
* Default settings are all Markdown features. Turn those off, only using the
|
||||
* inline HTML processor and HTML entities processor.
|
||||
*/
|
||||
MarkwonInlineParser.factoryBuilderNoDefaults()
|
||||
.addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
|
||||
.addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
|
||||
.usePlugin(
|
||||
MarkwonInlineParserPlugin.create(
|
||||
/* Configuring the Markwon inline formatting processor.
|
||||
* Default settings are all Markdown features. Turn those off, only using the
|
||||
* inline HTML processor and HTML entities processor.
|
||||
*/
|
||||
MarkwonInlineParser.factoryBuilderNoDefaults()
|
||||
.addInlineProcessor(HtmlInlineProcessor()) // use inline HTML processor
|
||||
.addInlineProcessor(EntityInlineProcessor()) // use HTML entities processor
|
||||
)
|
||||
)
|
||||
)
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureParser(builder: Parser.Builder) {
|
||||
/* Configuring the Markwon block formatting processor.
|
||||
* Default settings are all Markdown blocks. Turn those off.
|
||||
*/
|
||||
builder.enabledBlockTypes(kotlin.collections.emptySet())
|
||||
}
|
||||
})
|
||||
.textSetter(PrecomputedFutureTextSetterCompat.create())
|
||||
.build()
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureParser(builder: Parser.Builder) {
|
||||
/* Configuring the Markwon block formatting processor.
|
||||
* Default settings are all Markdown blocks. Turn those off.
|
||||
*/
|
||||
builder.enabledBlockTypes(kotlin.collections.emptySet())
|
||||
}
|
||||
})
|
||||
.textSetter(PrecomputedFutureTextSetterCompat.create())
|
||||
.build()
|
||||
|
||||
val plugins: List<MarkwonPlugin> = markwon.plugins
|
||||
|
||||
|
@ -26,9 +26,12 @@ import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.services.VectorAndroidService
|
||||
import im.vector.app.features.location.live.GetLiveLocationShareSummaryUseCase
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import im.vector.app.features.redaction.CheckIfEventIsRedactedUseCase
|
||||
import im.vector.app.features.session.coroutineScope
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
@ -55,6 +58,7 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
||||
@Inject lateinit var locationTracker: LocationTracker
|
||||
@Inject lateinit var activeSessionHolder: ActiveSessionHolder
|
||||
@Inject lateinit var getLiveLocationShareSummaryUseCase: GetLiveLocationShareSummaryUseCase
|
||||
@Inject lateinit var checkIfEventIsRedactedUseCase: CheckIfEventIsRedactedUseCase
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
@ -66,6 +70,9 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
||||
private val jobs = mutableListOf<Job>()
|
||||
private var startInProgress = false
|
||||
|
||||
private val _roomIdsOfActiveLives = MutableSharedFlow<Set<String>>(replay = 1)
|
||||
val roomIdsOfActiveLives = _roomIdsOfActiveLives.asSharedFlow()
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Timber.i("onCreate")
|
||||
@ -193,24 +200,30 @@ class LocationSharingAndroidService : VectorAndroidService(), LocationTracker.Ca
|
||||
private fun addRoomArgs(beaconEventId: String, roomArgs: RoomArgs) {
|
||||
Timber.i("adding roomArgs for beaconEventId: $beaconEventId")
|
||||
roomArgsMap[beaconEventId] = roomArgs
|
||||
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
|
||||
}
|
||||
|
||||
private fun removeRoomArgs(beaconEventId: String) {
|
||||
Timber.i("removing roomArgs for beaconEventId: $beaconEventId")
|
||||
roomArgsMap.remove(beaconEventId)
|
||||
launchWithActiveSession { _roomIdsOfActiveLives.emit(getRoomIdsOfActiveLives()) }
|
||||
}
|
||||
|
||||
private fun listenForLiveSummaryChanges(roomId: String, beaconEventId: String) {
|
||||
launchWithActiveSession { session ->
|
||||
val job = getLiveLocationShareSummaryUseCase.execute(roomId, beaconEventId)
|
||||
.distinctUntilChangedBy { it.isActive }
|
||||
.filter { it.isActive == false }
|
||||
.distinctUntilChangedBy { it?.isActive }
|
||||
.filter { it?.isActive == false || (it == null && isLiveRedacted(roomId, beaconEventId)) }
|
||||
.onEach { stopSharingLocation(beaconEventId) }
|
||||
.launchIn(session.coroutineScope)
|
||||
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) =
|
||||
activeSessionHolder
|
||||
.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 {
|
||||
return binder
|
||||
}
|
||||
|
@ -21,17 +21,22 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
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.Singleton
|
||||
|
||||
@Singleton
|
||||
class LocationSharingServiceConnection @Inject constructor(
|
||||
private val context: Context
|
||||
) : ServiceConnection,
|
||||
LocationSharingAndroidService.Callback {
|
||||
private val context: Context,
|
||||
private val activeSessionHolder: ActiveSessionHolder
|
||||
) : ServiceConnection, LocationSharingAndroidService.Callback {
|
||||
|
||||
interface Callback {
|
||||
fun onLocationServiceRunning()
|
||||
fun onLocationServiceRunning(roomIds: Set<String>)
|
||||
fun onLocationServiceStopped()
|
||||
fun onLocationServiceError(error: Throwable)
|
||||
}
|
||||
@ -44,7 +49,7 @@ class LocationSharingServiceConnection @Inject constructor(
|
||||
addCallback(callback)
|
||||
|
||||
if (isBound) {
|
||||
callback.onLocationServiceRunning()
|
||||
callback.onLocationServiceRunning(getRoomIdsOfActiveLives())
|
||||
} else {
|
||||
Intent(context, LocationSharingAndroidService::class.java).also { intent ->
|
||||
context.bindService(intent, this, 0)
|
||||
@ -56,12 +61,24 @@ class LocationSharingServiceConnection @Inject constructor(
|
||||
removeCallback(callback)
|
||||
}
|
||||
|
||||
private fun getRoomIdsOfActiveLives(): Set<String> {
|
||||
return locationSharingAndroidService?.getRoomIdsOfActiveLives() ?: emptySet()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
||||
locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also {
|
||||
it.callback = this
|
||||
locationSharingAndroidService = (binder as LocationSharingAndroidService.LocalBinder).getService().also { service ->
|
||||
service.callback = this
|
||||
getActiveSessionCoroutineScope()?.let { scope ->
|
||||
service.roomIdsOfActiveLives
|
||||
.onEach(::onRoomIdsUpdate)
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
isBound = true
|
||||
onCallbackActionNoArg(Callback::onLocationServiceRunning)
|
||||
}
|
||||
|
||||
private fun getActiveSessionCoroutineScope(): CoroutineScope? {
|
||||
return activeSessionHolder.getSafeActiveSession()?.coroutineScope
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
@ -71,6 +88,10 @@ class LocationSharingServiceConnection @Inject constructor(
|
||||
onCallbackActionNoArg(Callback::onLocationServiceStopped)
|
||||
}
|
||||
|
||||
private fun onRoomIdsUpdate(roomIds: Set<String>) {
|
||||
forwardRoomIdsToCallbacks(roomIds)
|
||||
}
|
||||
|
||||
override fun onServiceError(error: Throwable) {
|
||||
forwardErrorToCallbacks(error)
|
||||
}
|
||||
@ -87,6 +108,10 @@ class LocationSharingServiceConnection @Inject constructor(
|
||||
callbacks.toList().forEach(action)
|
||||
}
|
||||
|
||||
private fun forwardRoomIdsToCallbacks(roomIds: Set<String>) {
|
||||
callbacks.toList().forEach { it.onLocationServiceRunning(roomIds) }
|
||||
}
|
||||
|
||||
private fun forwardErrorToCallbacks(error: Throwable) {
|
||||
callbacks.toList().forEach { it.onLocationServiceError(error) }
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ package im.vector.app.features.location.live
|
||||
import androidx.lifecycle.asFlow
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
import org.matrix.android.sdk.api.session.getRoom
|
||||
@ -31,13 +31,13 @@ class GetLiveLocationShareSummaryUseCase @Inject constructor(
|
||||
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")
|
||||
session.getRoom(roomId)
|
||||
?.locationSharingService()
|
||||
?.getLiveLocationShareSummary(eventId)
|
||||
?.asFlow()
|
||||
?.mapNotNull { it.getOrNull() }
|
||||
?.map { it.getOrNull() }
|
||||
?: emptyFlow()
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ import im.vector.app.databinding.BottomSheetLiveLocationLabsFlagPromotionBinding
|
||||
* This should not be shown if the user already enabled the labs flag.
|
||||
*/
|
||||
class LiveLocationLabsFlagPromotionBottomSheet :
|
||||
VectorBaseBottomSheetDialogFragment<BottomSheetLiveLocationLabsFlagPromotionBinding>() {
|
||||
VectorBaseBottomSheetDialogFragment<BottomSheetLiveLocationLabsFlagPromotionBinding>() {
|
||||
|
||||
override val showExpanded = true
|
||||
|
||||
|
@ -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.Style
|
||||
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.SymbolOptions
|
||||
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.platform.VectorBaseFragment
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.core.utils.openLocation
|
||||
import im.vector.app.databinding.FragmentLocationLiveMapViewBinding
|
||||
import im.vector.app.features.location.UrlMapProvider
|
||||
import im.vector.app.features.location.zoomToBounds
|
||||
@ -120,6 +122,10 @@ class LocationLiveMapViewFragment @Inject constructor() : VectorBaseFragment<Fra
|
||||
this@LocationLiveMapViewFragment.mapboxMap = WeakReference(mapboxMap)
|
||||
symbolManager = SymbolManager(mapFragment.view as MapView, mapboxMap, style).apply {
|
||||
iconAllowOverlap = true
|
||||
addClickListener {
|
||||
onSymbolClicked(it)
|
||||
true
|
||||
}
|
||||
}
|
||||
pendingLiveLocations
|
||||
.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() =
|
||||
childFragmentManager.findFragmentByTag(MAP_FRAGMENT_TAG) as? SupportMapFragment
|
||||
?: run {
|
||||
|
@ -87,7 +87,7 @@ class LocationLiveMapViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLocationServiceRunning() {
|
||||
override fun onLocationServiceRunning(roomIds: Set<String>) {
|
||||
// NOOP
|
||||
}
|
||||
|
||||
|
@ -41,12 +41,12 @@ sealed interface AuthenticationDescription : Parcelable {
|
||||
}
|
||||
|
||||
fun SsoIdentityProvider?.toAuthenticationType() = when (this?.brand) {
|
||||
SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google
|
||||
SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub
|
||||
SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple
|
||||
SsoIdentityProvider.BRAND_GOOGLE -> AuthenticationType.Google
|
||||
SsoIdentityProvider.BRAND_GITHUB -> AuthenticationType.GitHub
|
||||
SsoIdentityProvider.BRAND_APPLE -> AuthenticationType.Apple
|
||||
SsoIdentityProvider.BRAND_FACEBOOK -> AuthenticationType.Facebook
|
||||
SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab
|
||||
SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO
|
||||
null -> AuthenticationType.SSO
|
||||
else -> AuthenticationType.SSO
|
||||
SsoIdentityProvider.BRAND_GITLAB -> AuthenticationType.GitLab
|
||||
SsoIdentityProvider.BRAND_TWITTER -> AuthenticationType.SSO
|
||||
null -> AuthenticationType.SSO
|
||||
else -> AuthenticationType.SSO
|
||||
}
|
||||
|
@ -52,9 +52,13 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||
object ResendResetPassword : 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 {
|
||||
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 LoginDirect(val matrixId: String, val password: String, val initialDeviceName: String) : AuthenticateAction
|
||||
}
|
||||
@ -71,6 +75,7 @@ sealed interface OnboardingAction : VectorViewModelAction {
|
||||
object ResetSignMode : ResetAction
|
||||
object ResetAuthenticationAttempt : ResetAction
|
||||
object ResetResetPassword : ResetAction
|
||||
object ResetSelectedRegistrationUserName : ResetAction
|
||||
|
||||
// Homeserver history
|
||||
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.configureAndStart
|
||||
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.platform.VectorViewModel
|
||||
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.SsoIdentityProvider
|
||||
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.failure.isHomeserverUnavailable
|
||||
import org.matrix.android.sdk.api.session.Session
|
||||
@ -144,7 +147,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
is OnboardingAction.UpdateSignMode -> handleUpdateSignMode(action)
|
||||
is OnboardingAction.InitWith -> handleInitWith(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 OnboardingAction.LoginWithToken -> handleLoginWithToken(action)
|
||||
is OnboardingAction.WebLoginSuccess -> handleWebLoginSuccess(action)
|
||||
@ -167,13 +170,47 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMaybeUpdateHomeserver(action: OnboardingAction.MaybeUpdateHomeserverFromMatrixId) {
|
||||
val isFullMatrixId = MatrixPatterns.isUserId(action.userId)
|
||||
private fun handleUserNameEntered(action: OnboardingAction.UserNameEnteredAction) {
|
||||
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) {
|
||||
val domain = action.userId.getServerName().substringBeforeLast(":").ensureProtocol()
|
||||
handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain))
|
||||
val domain = userNameOrMatrixId.getServerName().substringBeforeLast(":").ensureProtocol()
|
||||
handleHomeserverChange(OnboardingAction.HomeServerChange.EditHomeServer(domain), postAction = {
|
||||
val userName = MatrixPatterns.extractUserNameFromId(userNameOrMatrixId) ?: throw IllegalStateException("unexpected non matrix id")
|
||||
continuation(userName)
|
||||
})
|
||||
} 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) {
|
||||
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.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 {
|
||||
val authDescription = AuthenticationDescription.Register(AuthenticationDescription.AuthenticationType.Password)
|
||||
copy(selectedAuthenticationState = SelectedAuthenticationState(authDescription))
|
||||
}
|
||||
reAuthHelper.data = action.password
|
||||
reAuthHelper.data = password
|
||||
handleRegisterAction(
|
||||
RegisterAction.CreateAccount(
|
||||
action.username,
|
||||
action.password,
|
||||
action.initialDeviceName
|
||||
userName,
|
||||
password,
|
||||
initialDeviceName
|
||||
)
|
||||
)
|
||||
}
|
||||
@ -368,7 +410,12 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
OnboardingAction.ResetAuthenticationAttempt -> {
|
||||
viewModelScope.launch {
|
||||
authenticationService.cancelPendingLoginOrRegistration()
|
||||
setState { copy(isLoading = false) }
|
||||
setState {
|
||||
copy(
|
||||
isLoading = false,
|
||||
registrationState = RegistrationState(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
OnboardingAction.ResetResetPassword -> {
|
||||
@ -380,6 +427,11 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
}
|
||||
}
|
||||
OnboardingAction.ResetDeeplinkConfig -> loginConfig = null
|
||||
OnboardingAction.ResetSelectedRegistrationUserName -> {
|
||||
setState {
|
||||
copy(registrationState = RegistrationState())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -511,7 +563,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
setState { copy(isLoading = false, resetState = ResetState()) }
|
||||
val nextEvent = when {
|
||||
vectorFeatures.isOnboardingCombinedLoginEnabled() -> OnboardingViewEvents.OnResetPasswordComplete
|
||||
else -> OnboardingViewEvents.OpenResetPasswordComplete
|
||||
else -> OnboardingViewEvents.OpenResetPasswordComplete
|
||||
}
|
||||
_viewEvents.post(nextEvent)
|
||||
},
|
||||
@ -593,6 +645,7 @@ class OnboardingViewModel @AssistedInject constructor(
|
||||
val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
|
||||
val capabilityOverrides = vectorOverrides.forceHomeserverCapabilities?.firstOrNull()
|
||||
state.personalizationState.copy(
|
||||
displayName = state.registrationState.selectedMatrixId?.let { MatrixPatterns.extractUserNameFromId(it) },
|
||||
supportsChangingDisplayName = capabilityOverrides?.canChangeDisplayName ?: homeServerCapabilities.canChangeDisplayName,
|
||||
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)
|
||||
if (homeServerConnectionConfig == null) {
|
||||
// This is invalid
|
||||
_viewEvents.post(OnboardingViewEvents.Failure(Throwable("Unable to create a HomeServerConnectionConfig")))
|
||||
} else {
|
||||
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride)
|
||||
startAuthenticationFlow(action, homeServerConnectionConfig, serverTypeOverride, postAction)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAuthenticationFlow(
|
||||
trigger: OnboardingAction.HomeServerChange,
|
||||
homeServerConnectionConfig: HomeServerConnectionConfig,
|
||||
serverTypeOverride: ServerType?
|
||||
serverTypeOverride: ServerType?,
|
||||
postAction: suspend () -> Unit = {},
|
||||
) {
|
||||
currentHomeServerConnectionConfig = homeServerConnectionConfig
|
||||
|
||||
currentJob = viewModelScope.launch {
|
||||
setState { copy(isLoading = true) }
|
||||
runCatching { startAuthenticationFlowUseCase.execute(homeServerConnectionConfig) }.fold(
|
||||
onSuccess = { onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride) },
|
||||
onSuccess = {
|
||||
onAuthenticationStartedSuccess(trigger, homeServerConnectionConfig, it, serverTypeOverride)
|
||||
postAction()
|
||||
},
|
||||
onFailure = { onAuthenticationStartError(it, trigger) }
|
||||
)
|
||||
setState { copy(isLoading = false) }
|
||||
|
@ -48,6 +48,9 @@ data class OnboardingViewState(
|
||||
val knownCustomHomeServersUrls: List<String> = emptyList(),
|
||||
val isForceLoginFallbackEnabled: Boolean = false,
|
||||
|
||||
@PersistState
|
||||
val registrationState: RegistrationState = RegistrationState(),
|
||||
|
||||
@PersistState
|
||||
val selectedHomeserver: SelectedHomeserverState = SelectedHomeserverState(),
|
||||
|
||||
@ -66,7 +69,6 @@ enum class OnboardingFlow {
|
||||
|
||||
@Parcelize
|
||||
data class SelectedHomeserverState(
|
||||
val description: String? = null,
|
||||
val userFacingUrl: String? = null,
|
||||
val upstreamUrl: String? = null,
|
||||
val preferredLoginMode: LoginMode = LoginMode.Unknown,
|
||||
@ -96,3 +98,9 @@ data class ResetState(
|
||||
data class SelectedAuthenticationState(
|
||||
val description: AuthenticationDescription? = null,
|
||||
) : Parcelable
|
||||
|
||||
@Parcelize
|
||||
data class RegistrationState(
|
||||
val isUserNameAvailable: Boolean = false,
|
||||
val selectedMatrixId: String? = null,
|
||||
) : Parcelable
|
||||
|
@ -16,10 +16,7 @@
|
||||
|
||||
package im.vector.app.features.onboarding
|
||||
|
||||
import im.vector.app.R
|
||||
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 org.matrix.android.sdk.api.auth.AuthenticationService
|
||||
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
|
||||
@ -29,7 +26,6 @@ import javax.inject.Inject
|
||||
|
||||
class StartAuthenticationFlowUseCase @Inject constructor(
|
||||
private val authenticationService: AuthenticationService,
|
||||
private val stringProvider: StringProvider
|
||||
) {
|
||||
|
||||
suspend fun execute(config: HomeServerConnectionConfig): StartAuthenticationResult {
|
||||
@ -46,10 +42,6 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
||||
config: HomeServerConnectionConfig,
|
||||
preferredLoginMode: LoginMode
|
||||
) = 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(),
|
||||
upstreamUrl = authFlow.homeServerUrl,
|
||||
preferredLoginMode = preferredLoginMode,
|
||||
@ -57,8 +49,6 @@ class StartAuthenticationFlowUseCase @Inject constructor(
|
||||
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
|
||||
)
|
||||
|
||||
private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()
|
||||
|
||||
private fun LoginFlowResult.findPreferredLoginMode() = when {
|
||||
supportedLoginTypes.containsAllItems(LoginFlowTypes.SSO, LoginFlowTypes.PASSWORD) -> LoginMode.SsoAndPassword(ssoIdentityProviders)
|
||||
supportedLoginTypes.contains(LoginFlowTypes.SSO) -> LoginMode.Sso(ssoIdentityProviders)
|
||||
|
@ -16,15 +16,18 @@
|
||||
|
||||
package im.vector.app.features.onboarding.ftueauth
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.text.toSpannable
|
||||
import androidx.core.view.isVisible
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.animations.play
|
||||
import im.vector.app.core.di.ActiveSessionHolder
|
||||
import im.vector.app.core.utils.isAnimationEnabled
|
||||
import im.vector.app.core.utils.styleMatchingText
|
||||
import im.vector.app.databinding.FragmentFtueAccountCreatedBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewEvents
|
||||
@ -47,7 +50,9 @@ class FtueAuthAccountCreatedFragment @Inject constructor(
|
||||
}
|
||||
|
||||
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.accountCreatedTakeMeHome.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.lifecycle.lifecycleScope
|
||||
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.editText
|
||||
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.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
@ -60,14 +63,18 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
views.loginRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
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)) }
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.loginSubmit.setOnClickListener { submit() }
|
||||
observeContentChangesAndResetErrors(views.loginInput, views.loginPasswordInput, views.loginSubmit)
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
views.loginInput.clearErrorOnChange(viewLifecycleOwner)
|
||||
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() {
|
||||
@ -105,7 +112,6 @@ class FtueAuthCombinedLoginFragment @Inject constructor(
|
||||
setupAutoFill()
|
||||
|
||||
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||
views.selectedServerDescription.text = state.selectedHomeserver.description
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
|
@ -28,11 +28,14 @@ import androidx.lifecycle.lifecycleScope
|
||||
import com.airbnb.mvrx.withState
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
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.editText
|
||||
import im.vector.app.core.extensions.hasSurroundingSpaces
|
||||
import im.vector.app.core.extensions.hideKeyboard
|
||||
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.setOnFocusLostListener
|
||||
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.OnboardingViewEvents
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import org.matrix.android.sdk.api.auth.data.SsoIdentityProvider
|
||||
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.isUsernameInUse
|
||||
import org.matrix.android.sdk.api.failure.isWeakPassword
|
||||
import reactivecircus.flowbinding.android.widget.textChanges
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val MINIMUM_PASSWORD_LENGTH = 8
|
||||
|
||||
class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAuthFragment<FragmentFtueCombinedRegisterBinding>() {
|
||||
|
||||
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): FragmentFtueCombinedRegisterBinding {
|
||||
@ -69,15 +76,27 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||
views.createAccountRoot.realignPercentagesToParent()
|
||||
views.editServerButton.debouncedClicks { viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.EditServerSelection)) }
|
||||
views.createAccountPasswordInput.setOnImeDoneListener { submit() }
|
||||
|
||||
views.createAccountInput.onTextChange(viewLifecycleOwner) {
|
||||
viewModel.handle(OnboardingAction.ResetSelectedRegistrationUserName)
|
||||
views.createAccountEntryFooter.text = ""
|
||||
}
|
||||
|
||||
views.createAccountInput.setOnFocusLostListener {
|
||||
viewModel.handle(OnboardingAction.MaybeUpdateHomeserverFromMatrixId(views.createAccountInput.content()))
|
||||
viewModel.handle(OnboardingAction.UserNameEnteredAction.Registration(views.createAccountInput.content()))
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSubmitButton() {
|
||||
views.createAccountSubmit.setOnClickListener { submit() }
|
||||
observeContentChangesAndResetErrors(views.createAccountInput, views.createAccountPasswordInput, views.createAccountSubmit)
|
||||
.launchIn(viewLifecycleOwner.lifecycleScope)
|
||||
views.createAccountInput.clearErrorOnChange(viewLifecycleOwner)
|
||||
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() {
|
||||
@ -103,7 +122,12 @@ class FtueAuthCombinedRegisterFragment @Inject constructor() : AbstractSSOFtueAu
|
||||
}
|
||||
|
||||
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) {
|
||||
setupUi(state)
|
||||
setupAutoFill()
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.selectedServerName.text = state.selectedHomeserver.userFacingUrl.toReducedUrl()
|
||||
views.selectedServerDescription.text = state.selectedHomeserver.description
|
||||
|
||||
if (state.isLoading) {
|
||||
// Ensure password is hidden
|
||||
views.createAccountPasswordInput.editText().hidePassword()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupUi(state: OnboardingViewState) {
|
||||
views.createAccountEntryFooter.text = when {
|
||||
state.registrationState.isUserNameAvailable -> getString(
|
||||
R.string.ftue_auth_create_account_username_entry_footer,
|
||||
state.registrationState.selectedMatrixId
|
||||
)
|
||||
|
||||
else -> ""
|
||||
}
|
||||
|
||||
when (state.selectedHomeserver.preferredLoginMode) {
|
||||
is LoginMode.SsoAndPassword -> renderSsoProviders(state.deviceId, state.selectedHomeserver.preferredLoginMode.ssoIdentityProviders)
|
||||
else -> hideSsoProviders()
|
||||
|
@ -20,14 +20,17 @@ import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.associateContentStateWith
|
||||
import im.vector.app.core.extensions.autofillEmail
|
||||
import im.vector.app.core.extensions.clearErrorOnChange
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtueEmailInputBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import org.matrix.android.sdk.api.auth.registration.RegisterThreePid
|
||||
import javax.inject.Inject
|
||||
@ -56,6 +59,10 @@ class FtueAuthEmailEntryFragment @Inject constructor() : AbstractFtueAuthFragmen
|
||||
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) {
|
||||
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.editText
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtuePhoneInputBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
import im.vector.app.features.onboarding.RegisterAction
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
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) {
|
||||
views.phoneEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ class FtueAuthResetPasswordBreakerFragment : AbstractFtueAuthFragment<FragmentFt
|
||||
views.resetPasswordBreakerGradientContainer.setBackgroundResource(themeProvider.ftueBreakerBackground())
|
||||
views.resetPasswordBreakerTitle.text = getString(R.string.ftue_auth_reset_password_breaker_title)
|
||||
.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.resetPasswordBreakerFooter.debouncedClicks {
|
||||
viewModel.handle(OnboardingAction.PostViewEvent(OnboardingViewEvents.OnResetPasswordBreakerConfirmed))
|
||||
|
@ -21,13 +21,16 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.R
|
||||
import im.vector.app.core.extensions.associateContentStateWith
|
||||
import im.vector.app.core.extensions.clearErrorOnChange
|
||||
import im.vector.app.core.extensions.content
|
||||
import im.vector.app.core.extensions.isEmail
|
||||
import im.vector.app.core.extensions.setOnImeDoneListener
|
||||
import im.vector.app.core.extensions.toReducedUrl
|
||||
import im.vector.app.databinding.FragmentFtueResetPasswordEmailInputBinding
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
import im.vector.app.features.onboarding.OnboardingViewState
|
||||
|
||||
@AndroidEntryPoint
|
||||
class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment<FragmentFtueResetPasswordEmailInputBinding>() {
|
||||
@ -53,6 +56,13 @@ class FtueAuthResetPasswordEmailEntryFragment : AbstractFtueAuthFragment<Fragmen
|
||||
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) {
|
||||
views.emailEntryInput.error = errorFormatter.toHumanReadable(throwable)
|
||||
}
|
||||
|
@ -16,16 +16,10 @@
|
||||
|
||||
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.core.extensions.hasContentFlow
|
||||
import im.vector.app.features.login.SignMode
|
||||
import im.vector.app.features.onboarding.OnboardingAction
|
||||
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 {
|
||||
return when (this) {
|
||||
@ -36,23 +30,7 @@ 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()) {
|
||||
true -> R.drawable.bg_gradient_ftue_breaker
|
||||
true -> R.drawable.bg_gradient_ftue_breaker
|
||||
false -> R.drawable.bg_color_background
|
||||
}
|
||||
|
@ -114,7 +114,9 @@ class FtueAuthTermsFragment @Inject constructor(
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,6 @@ class BiometricAuthError(val code: Int, message: String) : Throwable(message) {
|
||||
val isAuthPermanentlyDisabledError: Boolean get() = code == BiometricPrompt.ERROR_LOCKOUT_PERMANENT
|
||||
|
||||
companion object {
|
||||
private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT)
|
||||
private val LOCKOUT_ERROR_CODES = arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT)
|
||||
}
|
||||
}
|
||||
|
@ -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.
|
||||
*/
|
||||
val canUseWeakBiometricAuth: Boolean get() =
|
||||
configuration.isWeakBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_WEAK) == BIOMETRIC_SUCCESS
|
||||
val canUseWeakBiometricAuth: Boolean
|
||||
get() =
|
||||
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.
|
||||
*/
|
||||
val canUseStrongBiometricAuth: Boolean get() =
|
||||
configuration.isStrongBiometricsEnabled && biometricManager.canAuthenticate(BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS
|
||||
val canUseStrongBiometricAuth: Boolean
|
||||
get() =
|
||||
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.).
|
||||
*/
|
||||
val canUseDeviceCredentialsAuth: Boolean get() =
|
||||
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
|
||||
val canUseDeviceCredentialsAuth: Boolean
|
||||
get() =
|
||||
configuration.isDeviceCredentialUnlockEnabled && biometricManager.canAuthenticate(DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS
|
||||
|
||||
/**
|
||||
* Returns true if any system authentication method (biometric weak/strong or device credentials) can be used.
|
||||
*/
|
||||
@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.
|
||||
@ -153,9 +157,9 @@ class BiometricHelper @Inject constructor(
|
||||
@SuppressLint("NewApi")
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun authenticateInternal(
|
||||
activity: FragmentActivity,
|
||||
checkSystemKeyExists: Boolean,
|
||||
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||
activity: FragmentActivity,
|
||||
checkSystemKeyExists: Boolean,
|
||||
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||
): Flow<Boolean> {
|
||||
if (checkSystemKeyExists && !isSystemAuthEnabledAndValid) return flowOf(false)
|
||||
|
||||
@ -189,9 +193,9 @@ class BiometricHelper @Inject constructor(
|
||||
|
||||
@VisibleForTesting(otherwise = PRIVATE)
|
||||
internal fun authenticateWithPromptInternal(
|
||||
activity: FragmentActivity,
|
||||
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||
channel: Channel<Boolean>,
|
||||
activity: FragmentActivity,
|
||||
cryptoObject: BiometricPrompt.CryptoObject? = null,
|
||||
channel: Channel<Boolean>,
|
||||
): BiometricPrompt {
|
||||
val executor = ContextCompat.getMainExecutor(context)
|
||||
val callback = createSuspendingAuthCallback(channel, executor.asCoroutineDispatcher())
|
||||
|
@ -36,7 +36,7 @@ class LockScreenKeyRepository(
|
||||
private val systemKeyAlias = "$baseName.system"
|
||||
|
||||
private val pinCodeCrypto: KeyStoreCrypto by lazy {
|
||||
keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false)
|
||||
keyStoreCryptoFactory.provide(pinCodeKeyAlias, keyNeedsUserAuthentication = false)
|
||||
}
|
||||
private val systemKeyCrypto: KeyStoreCrypto by lazy {
|
||||
keyStoreCryptoFactory.provide(systemKeyAlias, keyNeedsUserAuthentication = true)
|
||||
|
@ -114,15 +114,15 @@ class LockScreenFragment : VectorBaseFragment<FragmentLockScreenBinding>() {
|
||||
private fun handleEvent(viewEvent: LockScreenViewEvent) {
|
||||
when (viewEvent) {
|
||||
is LockScreenViewEvent.CodeCreationComplete -> lockScreenListener?.onPinCodeCreated()
|
||||
is LockScreenViewEvent.ClearPinCode -> {
|
||||
is LockScreenViewEvent.ClearPinCode -> {
|
||||
if (viewEvent.confirmationFailed) {
|
||||
lockScreenListener?.onNewCodeValidationFailed()
|
||||
}
|
||||
views.codeView.clearCode()
|
||||
}
|
||||
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
|
||||
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
|
||||
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
|
||||
is LockScreenViewEvent.AuthSuccessful -> lockScreenListener?.onAuthenticationSuccess(viewEvent.method)
|
||||
is LockScreenViewEvent.AuthFailure -> onAuthFailure(viewEvent.method)
|
||||
is LockScreenViewEvent.AuthError -> onAuthError(viewEvent.method, viewEvent.throwable)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,7 +94,7 @@ class LockScreenViewModel @AssistedInject constructor(
|
||||
|
||||
override fun handle(action: LockScreenAction) {
|
||||
when (action) {
|
||||
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
|
||||
is LockScreenAction.PinCodeEntered -> onPinCodeEntered(action.value)
|
||||
is LockScreenAction.ShowBiometricPrompt -> showBiometricPrompt(action.callingActivity)
|
||||
}
|
||||
}
|
||||
|
@ -48,15 +48,15 @@ object DevicePromptCheck {
|
||||
* See [this OP forum thread](https://forums.oneplus.com/threads/oneplus-7-pro-fingerprint-biometricprompt-does-not-show.1035821/).
|
||||
*/
|
||||
private val isOnePlusDeviceWithNoBiometricUI: Boolean =
|
||||
Build.BRAND.equals("OnePlus", ignoreCase = true) &&
|
||||
!onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
Build.BRAND.equals("OnePlus", ignoreCase = true) &&
|
||||
!onePlusModelsWithWorkingBiometricUI.contains(Build.MODEL) &&
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.R
|
||||
|
||||
/**
|
||||
* Some LG models don't seem to have a system biometric prompt at all.
|
||||
*/
|
||||
private val isLGDeviceWithNoBiometricUI: Boolean =
|
||||
Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL)
|
||||
Build.BRAND.equals("LG", ignoreCase = true) && lgModelsWithoutBiometricUI.contains(Build.MODEL)
|
||||
|
||||
/**
|
||||
* Check if this device is included in the list of devices with known Biometric Prompt issues.
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
@ -31,7 +31,7 @@ import javax.inject.Inject
|
||||
|
||||
class FontScaleSettingFragment @Inject constructor(
|
||||
private val fontListController: FontScaleSettingController
|
||||
) : VectorBaseFragment<FragmentSettingsFontScalingBinding>(), FontScaleSettingController.Callback {
|
||||
) : VectorBaseFragment<FragmentSettingsFontScalingBinding>(), FontScaleSettingController.Callback {
|
||||
|
||||
private val viewModel: FontScaleSettingViewModel by fragmentViewModel()
|
||||
|
||||
|
@ -94,10 +94,10 @@ class SoftLogoutController @Inject constructor(
|
||||
}
|
||||
|
||||
private fun buildForm(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest) {
|
||||
is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error)
|
||||
is Success -> buildLoginSuccessItem(state)
|
||||
is Fail -> buildLoginErrorWithRetryItem(state.asyncHomeServerLoginFlowRequest.error)
|
||||
is Success -> buildLoginSuccessItem(state)
|
||||
is Loading, Uninitialized -> buildLoadingItem()
|
||||
is Incomplete -> Unit
|
||||
is Incomplete -> Unit
|
||||
}
|
||||
|
||||
private fun buildLoadingItem() {
|
||||
@ -116,11 +116,11 @@ class SoftLogoutController @Inject constructor(
|
||||
}
|
||||
|
||||
private fun buildLoginSuccessItem(state: SoftLogoutViewState) = when (state.asyncHomeServerLoginFlowRequest.invoke()) {
|
||||
LoginMode.Password -> buildLoginPasswordForm(state)
|
||||
is LoginMode.Sso -> buildLoginSSOForm()
|
||||
LoginMode.Password -> buildLoginPasswordForm(state)
|
||||
is LoginMode.Sso -> buildLoginSSOForm()
|
||||
is LoginMode.SsoAndPassword -> disambiguateLoginSSOAndPasswordForm(state)
|
||||
LoginMode.Unsupported -> buildLoginUnsupportedForm()
|
||||
LoginMode.Unknown, null -> Unit // Should not happen
|
||||
LoginMode.Unsupported -> buildLoginUnsupportedForm()
|
||||
LoginMode.Unknown, null -> Unit // Should not happen
|
||||
}
|
||||
|
||||
private fun buildLoginPasswordForm(state: SoftLogoutViewState) {
|
||||
@ -148,12 +148,12 @@ class SoftLogoutController @Inject constructor(
|
||||
|
||||
private fun disambiguateLoginSSOAndPasswordForm(state: SoftLogoutViewState) {
|
||||
when (state.loginType) {
|
||||
LoginType.PASSWORD -> buildLoginPasswordForm(state)
|
||||
LoginType.SSO -> buildLoginSSOForm()
|
||||
LoginType.PASSWORD -> buildLoginPasswordForm(state)
|
||||
LoginType.SSO -> buildLoginSSOForm()
|
||||
LoginType.DIRECT,
|
||||
LoginType.CUSTOM,
|
||||
LoginType.UNSUPPORTED -> buildLoginUnsupportedForm()
|
||||
LoginType.UNKNOWN -> Unit
|
||||
LoginType.UNKNOWN -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,6 +110,7 @@ class SpaceDirectoryController @Inject constructor(
|
||||
?.filter {
|
||||
it.parentRoomId == (data.hierarchyStack.lastOrNull() ?: data.spaceId)
|
||||
}
|
||||
?.filterNot { it.isUpgradedRoom(data) }
|
||||
?: emptyList()
|
||||
|
||||
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() }
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user