Merge pull request #767 from vector-im/dm_verif_incoming_timeline

Dm verif incoming timeline
This commit is contained in:
Valere 2019-12-19 10:12:55 +01:00 committed by GitHub
commit d97402f757
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
193 changed files with 6464 additions and 859 deletions

View File

@ -18,6 +18,7 @@
<w>pbkdf</w> <w>pbkdf</w>
<w>pkcs</w> <w>pkcs</w>
<w>signin</w> <w>signin</w>
<w>signout</w>
<w>signup</w> <w>signup</w>
</words> </words>
</dictionary> </dictionary>

View File

@ -2,22 +2,24 @@ Changes in RiotX 0.11.0 (2019-XX-XX)
=================================================== ===================================================
Features ✨: Features ✨:
- - Implement soft logout (#281)
Improvements 🙌: Improvements 🙌:
- -
Other changes: Other changes:
- - Use same default room colors than Riot-Web
Bugfix 🐛: Bugfix 🐛:
- - Scroll breadcrumbs to top when opened
- Render default room name when it starts with an emoji (#477)
- Do not display " (IRC)") in display names https://github.com/vector-im/riot-android/issues/444
Translations 🗣: Translations 🗣:
- -
Build 🧱: Build 🧱:
- - Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10) Changes in RiotX 0.10.0 (2019-12-10)
=================================================== ===================================================

View File

@ -10,7 +10,7 @@ buildscript {
} }
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:3.5.1' classpath 'com.android.tools.build:gradle:3.5.3'
classpath 'com.google.gms:google-services:4.3.2' classpath 'com.google.gms:google-services:4.3.2'
classpath "com.airbnb.okreplay:gradle-plugin:1.5.0" classpath "com.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@ -45,12 +45,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google() google()
jcenter() jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
} }
tasks.withType(JavaCompile).all { tasks.withType(JavaCompile).all {

1
diff-match-patch/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

View File

@ -0,0 +1,8 @@
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "8"
targetCompatibility = "8"

File diff suppressed because it is too large Load Diff

View File

@ -22,5 +22,6 @@ package im.vector.matrix.android.api.auth.data
*/ */
data class SessionParams( data class SessionParams(
val credentials: Credentials, val credentials: Credentials,
val homeServerConnectionConfig: HomeServerConnectionConfig val homeServerConnectionConfig: HomeServerConnectionConfig,
val isTokenValid: Boolean
) )

View File

@ -16,7 +16,8 @@
package im.vector.matrix.android.api.failure package im.vector.matrix.android.api.failure
// This data class will be sent to the bus // This class will be sent to the bus
data class ConsentNotGivenError( sealed class GlobalError {
val consentUri: String data class InvalidToken(val softLogout: Boolean) : GlobalError()
) data class ConsentNotGivenError(val consentUri: String) : GlobalError()
}

View File

@ -22,45 +22,112 @@ import com.squareup.moshi.JsonClass
/** /**
* This data class holds the error defined by the matrix specifications. * This data class holds the error defined by the matrix specifications.
* You shouldn't have to instantiate it. * You shouldn't have to instantiate it.
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/ */
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MatrixError( data class MatrixError(
/** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String, @Json(name = "errcode") val code: String,
/** human-readable error message */
@Json(name = "error") val message: String, @Json(name = "error") val message: String,
// For M_CONSENT_NOT_GIVEN
@Json(name = "consent_uri") val consentUri: String? = null, @Json(name = "consent_uri") val consentUri: String? = null,
// RESOURCE_LIMIT_EXCEEDED data // For M_RESOURCE_LIMIT_EXCEEDED
@Json(name = "limit_type") val limitType: String? = null, @Json(name = "limit_type") val limitType: String? = null,
@Json(name = "admin_contact") val adminUri: String? = null, @Json(name = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED // For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) { @Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
) {
companion object { companion object {
const val FORBIDDEN = "M_FORBIDDEN" /** Forbidden access, e.g. joining a room without permission, failed login. */
const val UNKNOWN = "M_UNKNOWN" const val M_FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN" /** An unknown error has occurred. */
const val MISSING_TOKEN = "M_MISSING_TOKEN" const val M_UNKNOWN = "M_UNKNOWN"
const val BAD_JSON = "M_BAD_JSON" /** The access token specified was not recognised. */
const val NOT_JSON = "M_NOT_JSON" const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val NOT_FOUND = "M_NOT_FOUND" /** No access token was specified for the request. */
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED" const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
const val USER_IN_USE = "M_USER_IN_USE" /** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
const val ROOM_IN_USE = "M_ROOM_IN_USE" const val M_BAD_JSON = "M_BAD_JSON"
const val BAD_PAGINATION = "M_BAD_PAGINATION" /** Request did not contain valid JSON. */
const val UNAUTHORIZED = "M_UNAUTHORIZED" const val M_NOT_JSON = "M_NOT_JSON"
const val OLD_VERSION = "M_OLD_VERSION" /** No resource was found for this request. */
const val UNRECOGNIZED = "M_UNRECOGNIZED" const val M_NOT_FOUND = "M_NOT_FOUND"
/** Too many requests have been sent in a short period of time. Wait a while then try again. */
const val M_LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET" /* ==========================================================================================
const val THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED" * Other error codes the client might encounter are
// Error code returned by the server when no account matches the given 3pid * ========================================================================================== */
const val THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
const val THREEPID_IN_USE = "M_THREEPID_IN_USE" /** Encountered when trying to register a user ID which has been taken. */
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED" const val M_USER_IN_USE = "M_USER_IN_USE"
const val TOO_LARGE = "M_TOO_LARGE" /** Sent when the room alias given to the createRoom API is already in use. */
const val M_ROOM_IN_USE = "M_ROOM_IN_USE"
/** (Not documented yet) */
const val M_BAD_PAGINATION = "M_BAD_PAGINATION"
/** The request was not correctly authorized. Usually due to login failures. */
const val M_UNAUTHORIZED = "M_UNAUTHORIZED"
/** (Not documented yet) */
const val M_OLD_VERSION = "M_OLD_VERSION"
/** The server did not understand the request. */
const val M_UNRECOGNIZED = "M_UNRECOGNIZED"
/** (Not documented yet) */
const val M_LOGIN_EMAIL_URL_NOT_YET = "M_LOGIN_EMAIL_URL_NOT_YET"
/** Authentication could not be performed on the third party identifier. */
const val M_THREEPID_AUTH_FAILED = "M_THREEPID_AUTH_FAILED"
/** Sent when a threepid given to an API cannot be used because no record matching the threepid was found. */
const val M_THREEPID_NOT_FOUND = "M_THREEPID_NOT_FOUND"
/** Sent when a threepid given to an API cannot be used because the same threepid is already in use. */
const val M_THREEPID_IN_USE = "M_THREEPID_IN_USE"
/** The client's request used a third party server, eg. identity server, that this server does not trust. */
const val M_SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
/** The request or entity was too large. */
const val M_TOO_LARGE = "M_TOO_LARGE"
/** (Not documented yet) */
const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN" const val M_CONSENT_NOT_GIVEN = "M_CONSENT_NOT_GIVEN"
const val RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED" /** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION" * a homeserver held in a shared hosting environment may reach a resource limit if it starts using too much memory
* or disk space. The error MUST have an admin_contact field to provide the user receiving the error a place to reach
* out to. Typically, this error will appear on routes which attempt to modify state (eg: sending messages, account
* data, etc) and not routes which only read state (eg: /sync, get account data, etc). */
const val M_RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
/** The user ID associated with the request has been deactivated. Typically for endpoints that prove authentication, such as /login. */
const val M_USER_DEACTIVATED = "M_USER_DEACTIVATED"
/** Encountered when trying to register a user ID which is not valid. */
const val M_INVALID_USERNAME = "M_INVALID_USERNAME"
/** Sent when the initial state given to the createRoom API is invalid. */
const val M_INVALID_ROOM_STATE = "M_INVALID_ROOM_STATE"
/** The server does not permit this third party identifier. This may happen if the server only permits,
* for example, email addresses from a particular domain. */
const val M_THREEPID_DENIED = "M_THREEPID_DENIED"
/** The client's request to create a room used a room version that the server does not support. */
const val M_UNSUPPORTED_ROOM_VERSION = "M_UNSUPPORTED_ROOM_VERSION"
/** The client attempted to join a room that has a version the server does not support.
* Inspect the room_version property of the error response for the room's version. */
const val M_INCOMPATIBLE_ROOM_VERSION = "M_INCOMPATIBLE_ROOM_VERSION"
/** The state change requested cannot be performed, such as attempting to unban a user who is not banned. */
const val M_BAD_STATE = "M_BAD_STATE"
/** The room or resource does not permit guests to access it. */
const val M_GUEST_ACCESS_FORBIDDEN = "M_GUEST_ACCESS_FORBIDDEN"
/** A Captcha is required to complete the request. */
const val M_CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
/** The Captcha provided did not match what was expected. */
const val M_CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
/** A required parameter was missing from the request. */
const val M_MISSING_PARAM = "M_MISSING_PARAM"
/** A parameter that was specified has the wrong value. For example, the server expected an integer and instead received a string. */
const val M_INVALID_PARAM = "M_INVALID_PARAM"
/** The resource being requested is reserved by an application service, or the application service making the request has not created the resource. */
const val M_EXCLUSIVE = "M_EXCLUSIVE"
/** The user is unable to reject an invite to join the server notices room. See the Server Notices module for more information. */
const val M_CANNOT_LEAVE_SERVER_NOTICE_ROOM = "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM"
/** (Not documented yet) */
const val M_WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
// Possible value for "limit_type" // Possible value for "limit_type"
const val LIMIT_TYPE_MAU = "monthly_active_user" const val LIMIT_TYPE_MAU = "monthly_active_user"

View File

@ -19,7 +19,7 @@ package im.vector.matrix.android.api.session
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.cache.CacheService import im.vector.matrix.android.api.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -62,6 +62,11 @@ interface Session :
*/ */
val sessionParams: SessionParams val sessionParams: SessionParams
/**
* The session is valid, i.e. it has a valid token so far
*/
val isOpenable: Boolean
/** /**
* Useful shortcut to get access to the userId * Useful shortcut to get access to the userId
*/ */
@ -81,7 +86,7 @@ interface Session :
/** /**
* Launches infinite periodic background syncs * Launches infinite periodic background syncs
* THis does not work in doze mode :/ * This does not work in doze mode :/
* If battery optimization is on it can work in app standby but that's all :/ * If battery optimization is on it can work in app standby but that's all :/
*/ */
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L) fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
@ -136,13 +141,10 @@ interface Session :
*/ */
interface Listener { interface Listener {
/** /**
* The access token is not valid anymore * Possible cases:
* - The access token is not valid anymore,
* - a M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/ */
fun onInvalidToken() fun onGlobalError(globalError: GlobalError)
/**
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
} }
} }

View File

@ -24,7 +24,7 @@ import im.vector.matrix.android.api.session.room.model.relation.RelationDefaultC
import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel import im.vector.matrix.android.internal.crypto.verification.VerificationInfoCancel
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
internal data class MessageVerificationCancelContent( data class MessageVerificationCancelContent(
@Json(name = "code") override val code: String? = null, @Json(name = "code") override val code: String? = null,
@Json(name = "reason") override val reason: String? = null, @Json(name = "reason") override val reason: String? = null,
@Json(name = "m.relates_to") val relatesTo: RelationDefaultContent? @Json(name = "m.relates_to") val relatesTo: RelationDefaultContent?

View File

@ -16,11 +16,12 @@
package im.vector.matrix.android.api.session.room.send package im.vector.matrix.android.api.session.room.send
import im.vector.matrix.android.api.util.MatrixItem
/** /**
* Tag class for spans that should mention a user. * Tag class for spans that should mention a user.
* These Spans will be transformed into pills when detected in message to send * These Spans will be transformed into pills when detected in message to send
*/ */
interface UserMentionSpan { interface UserMentionSpan {
val displayName: String val matrixItem: MatrixItem
val userId: String
} }

View File

@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout package im.vector.matrix.android.api.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.util.Cancelable
/** /**
* This interface defines a method to sign out. It's implemented at the session level. * This interface defines a method to sign out, or to renew the token. It's implemented at the session level.
*/ */
interface SignOutService { interface SignOutService {
/** /**
* Sign out * Ask the homeserver for a new access token.
* The same deviceId will be used
*/ */
fun signOut(callback: MatrixCallback<Unit>) fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable
/**
* Update the session with credentials received after SSO
*/
fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable
/**
* Sign out, and release the session, clear all the session data, including crypto data
* @param sigOutFromHomeserver true if the sign out request has to be done
*/
fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable
} }

View File

@ -17,10 +17,11 @@
package im.vector.matrix.android.api.session.sync package im.vector.matrix.android.api.session.sync
sealed class SyncState { sealed class SyncState {
object IDLE : SyncState() object Idle : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState() data class Running(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState() object Paused : SyncState()
object KILLING : SyncState() object Killing : SyncState()
object KILLED : SyncState() object Killed : SyncState()
object NO_NETWORK : SyncState() object NoNetwork : SyncState()
object InvalidToken : SyncState()
} }

View File

@ -0,0 +1,141 @@
/*
* Copyright 2019 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.matrix.android.api.util
import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoom
import im.vector.matrix.android.api.session.user.model.User
import java.util.*
sealed class MatrixItem(
open val id: String,
open val displayName: String?,
open val avatarUrl: String?
) {
data class UserItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName?.removeSuffix(ircPattern), avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class EventItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class RoomAliasItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
data class GroupItem(override val id: String,
override val displayName: String? = null,
override val avatarUrl: String? = null)
: MatrixItem(id, displayName, avatarUrl) {
init {
if (BuildConfig.DEBUG) checkId()
}
}
fun getBestName(): String {
return displayName?.takeIf { it.isNotBlank() } ?: id
}
protected fun checkId() {
if (!id.startsWith(getIdPrefix())) {
error("Wrong usage of MatrixItem: check the id $id should start with ${getIdPrefix()}")
}
}
/**
* Return the prefix as defined in the matrix spec (and not extracted from the id)
*/
fun getIdPrefix() = when (this) {
is UserItem -> '@'
is EventItem -> '$'
is RoomItem -> '!'
is RoomAliasItem -> '#'
is GroupItem -> '+'
}
fun firstLetterOfDisplayName(): String {
return getBestName()
.let { dn ->
var startIndex = 0
val initial = dn[startIndex]
if (initial in listOf('@', '#', '+') && dn.length > 1) {
startIndex++
}
var length = 1
var first = dn[startIndex]
// LEFT-TO-RIGHT MARK
if (dn.length >= 2 && 0x200e == first.toInt()) {
startIndex++
first = dn[startIndex]
}
// check if its the start of a surrogate pair
if (first.toInt() in 0xD800..0xDBFF && dn.length > startIndex + 1) {
val second = dn[startIndex + 1]
if (second.toInt() in 0xDC00..0xDFFF) {
length++
}
}
dn.substring(startIndex, startIndex + length)
}
.toUpperCase(Locale.ROOT)
}
companion object {
private const val ircPattern = " (IRC)"
}
}
/* ==========================================================================================
* Extensions to create MatrixItem
* ========================================================================================== */
fun User.toMatrixItem() = MatrixItem.UserItem(userId, displayName, avatarUrl)
fun GroupSummary.toMatrixItem() = MatrixItem.GroupItem(groupId, displayName, avatarUrl)
fun RoomSummary.toMatrixItem() = MatrixItem.RoomItem(roomId, displayName, avatarUrl)
fun PublicRoom.toMatrixItem() = MatrixItem.RoomItem(roomId, name, avatarUrl)

View File

@ -53,7 +53,7 @@ internal abstract class AuthModule {
.name("matrix-sdk-auth.realm") .name("matrix-sdk-auth.realm")
.modules(AuthRealmModule()) .modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION) .schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration()) .migration(AuthRealmMigration)
.build() .build()
} }
} }

View File

@ -60,7 +60,8 @@ internal class DefaultSessionCreator @Inject constructor(
?.also { Timber.d("Overriding identity server url to $it") } ?.also { Timber.d("Overriding identity server url to $it") }
?.let { Uri.parse(it) } ?.let { Uri.parse(it) }
?: homeServerConnectionConfig.identityServerUri ?: homeServerConnectionConfig.identityServerUri
)) ),
isTokenValid = true)
sessionParamsStore.save(sessionParams) sessionParamsStore.save(sessionParams)
return sessionManager.getOrCreateSession(sessionParams) return sessionManager.getOrCreateSession(sessionParams)

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth package im.vector.matrix.android.internal.auth
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
internal interface SessionParamsStore { internal interface SessionParamsStore {
@ -28,6 +29,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams) suspend fun save(sessionParams: SessionParams)
suspend fun setTokenInvalid(userId: String)
suspend fun updateCredentials(newCredentials: Credentials)
suspend fun delete(userId: String) suspend fun delete(userId: String)
suspend fun deleteAll() suspend fun deleteAll()

View File

@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
internal class AuthRealmMigration : RealmMigration { internal object AuthRealmMigration : RealmMigration {
companion object { // Current schema version
// Current schema version const val SCHEMA_VERSION = 2L
const val SCHEMA_VERSION = 1L
}
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Auth Realm from $oldVersion to $newVersion") Timber.d("Migrating Auth Realm from $oldVersion to $newVersion")
@ -46,5 +44,14 @@ internal class AuthRealmMigration : RealmMigration {
.addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java) .addField(PendingSessionEntityFields.IS_REGISTRATION_STARTED, Boolean::class.java)
.addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java) .addField(PendingSessionEntityFields.CURRENT_THREE_PID_DATA_JSON, String::class.java)
} }
if (oldVersion <= 1) {
Timber.d("Step 1 -> 2")
Timber.d("Add boolean isTokenValid in SessionParamsEntity, with value true")
realm.schema.get("SessionParamsEntity")
?.addField(SessionParamsEntityFields.IS_TOKEN_VALID, Boolean::class.java)
?.transform { it.set(SessionParamsEntityFields.IS_TOKEN_VALID, true) }
}
} }
} }

View File

@ -16,6 +16,7 @@
package im.vector.matrix.android.internal.auth.db package im.vector.matrix.android.internal.auth.db
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.database.awaitTransaction import im.vector.matrix.android.internal.database.awaitTransaction
@ -75,6 +76,53 @@ internal class RealmSessionParamsStore @Inject constructor(private val mapper: S
} }
} }
override suspend fun setTokenInvalid(userId: String) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, userId)
.findAll()
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user $userId"
.let { Timber.w(it) }
.also { error(it) }
} else {
currentSessionParams.isTokenValid = false
}
}
}
override suspend fun updateCredentials(newCredentials: Credentials) {
awaitTransaction(realmConfiguration) { realm ->
val currentSessionParams = realm
.where(SessionParamsEntity::class.java)
.equalTo(SessionParamsEntityFields.USER_ID, newCredentials.userId)
.findAll()
.map { mapper.map(it) }
.firstOrNull()
if (currentSessionParams == null) {
// Should not happen
"Session param not found for user ${newCredentials.userId}"
.let { Timber.w(it) }
.also { error(it) }
} else {
val newSessionParams = currentSessionParams.copy(
credentials = newCredentials,
isTokenValid = true
)
val entity = mapper.map(newSessionParams)
if (entity != null) {
realm.insertOrUpdate(entity)
}
}
}
}
override suspend fun delete(userId: String) { override suspend fun delete(userId: String) {
awaitTransaction(realmConfiguration) { awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java) it.where(SessionParamsEntity::class.java)

View File

@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity( internal open class SessionParamsEntity(
@PrimaryKey var userId: String = "", @PrimaryKey var userId: String = "",
var credentialsJson: String = "", var credentialsJson: String = "",
var homeServerConnectionConfigJson: String = "" var homeServerConnectionConfigJson: String = "",
// Set to false when the token is invalid and the user has been soft logged out
// In case of hard logout, this object is deleted from DB
var isTokenValid: Boolean = true
) : RealmObject() ) : RealmObject()

View File

@ -36,7 +36,7 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentials == null || homeServerConnectionConfig == null) { if (credentials == null || homeServerConnectionConfig == null) {
return null return null
} }
return SessionParams(credentials, homeServerConnectionConfig) return SessionParams(credentials, homeServerConnectionConfig, entity.isTokenValid)
} }
fun map(sessionParams: SessionParams?): SessionParamsEntity? { fun map(sessionParams: SessionParams?): SessionParamsEntity? {
@ -48,6 +48,10 @@ internal class SessionParamsMapper @Inject constructor(moshi: Moshi) {
if (credentialsJson == null || homeServerConnectionConfigJson == null) { if (credentialsJson == null || homeServerConnectionConfigJson == null) {
return null return null
} }
return SessionParamsEntity(sessionParams.credentials.userId, credentialsJson, homeServerConnectionConfigJson) return SessionParamsEntity(
sessionParams.credentials.userId,
credentialsJson,
homeServerConnectionConfigJson,
sessionParams.isTokenValid)
} }
} }

View File

@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& failure.error.code == MatrixError.NOT_FOUND) { && failure.error.code == MatrixError.M_NOT_FOUND) {
// Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup // Workaround because the homeserver currently returns M_NOT_FOUND when there is no key backup
callback.onSuccess(null) callback.onSuccess(null)
} else { } else {
@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.") Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) { when (failure.error.code) {
MatrixError.NOT_FOUND, MatrixError.M_NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> { MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version // Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure) backupAllGroupSessionsCallback?.onFailure(failure)

View File

@ -29,12 +29,11 @@ import javax.inject.Inject
internal interface RequestVerificationDMTask : Task<RequestVerificationDMTask.Params, SendResponse> { internal interface RequestVerificationDMTask : Task<RequestVerificationDMTask.Params, SendResponse> {
data class Params( data class Params(
val roomId: String, val event: Event,
val from: String,
val methods: List<String>,
val to: String,
val cryptoService: CryptoService val cryptoService: CryptoService
) )
fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService): Params
} }
internal class DefaultRequestVerificationDMTask @Inject constructor( internal class DefaultRequestVerificationDMTask @Inject constructor(
@ -45,8 +44,18 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
private val roomAPI: RoomAPI) private val roomAPI: RoomAPI)
: RequestVerificationDMTask { : RequestVerificationDMTask {
override fun createParamsAndLocalEcho(roomId: String, from: String, methods: List<String>, to: String, cryptoService: CryptoService)
: RequestVerificationDMTask.Params {
val event = localEchoEventFactory.createVerificationRequest(roomId, from, to, methods)
.also { localEchoEventFactory.saveLocalEcho(monarchy, it) }
return RequestVerificationDMTask.Params(
event,
cryptoService
)
}
override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse { override suspend fun execute(params: RequestVerificationDMTask.Params): SendResponse {
val event = createRequestEvent(params) val event = handleEncryption(params)
val localID = event.eventId!! val localID = event.eventId!!
try { try {
@ -54,7 +63,7 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
val executeRequest = executeRequest<SendResponse> { val executeRequest = executeRequest<SendResponse> {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localID, localID,
roomId = params.roomId, roomId = event.roomId ?: "",
content = event.content, content = event.content,
eventType = event.type // message or room.encrypted eventType = event.type // message or room.encrypted
) )
@ -67,14 +76,13 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
} }
} }
private suspend fun createRequestEvent(params: RequestVerificationDMTask.Params): Event { private suspend fun handleEncryption(params: RequestVerificationDMTask.Params): Event {
val event = localEchoEventFactory.createVerificationRequest(params.roomId, params.from, params.to, params.methods) val roomId = params.event.roomId ?: ""
.also { localEchoEventFactory.saveLocalEcho(monarchy, it) } if (params.cryptoService.isRoomEncrypted(roomId)) {
if (params.cryptoService.isRoomEncrypted(params.roomId)) {
try { try {
return encryptEventTask.execute(EncryptEventTask.Params( return encryptEventTask.execute(EncryptEventTask.Params(
params.roomId, roomId,
event, params.event,
listOf("m.relates_to"), listOf("m.relates_to"),
params.cryptoService params.cryptoService
)) ))
@ -82,6 +90,6 @@ internal class DefaultRequestVerificationDMTask @Inject constructor(
// We said it's ok to send verification request in clear // We said it's ok to send verification request in clear
} }
} }
return event return params.event
} }
} }

View File

@ -34,10 +34,14 @@ import javax.inject.Inject
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, SendResponse> { internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, SendResponse> {
data class Params( data class Params(
val type: String, val type: String,
val roomId: String, val event: Event,
val content: Content,
val cryptoService: CryptoService? val cryptoService: CryptoService?
) )
fun createParamsAndLocalEcho(type: String,
roomId: String,
content: Content,
cryptoService: CryptoService?) : Params
} }
internal class DefaultSendVerificationMessageTask @Inject constructor( internal class DefaultSendVerificationMessageTask @Inject constructor(
@ -48,8 +52,28 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
@UserId private val userId: String, @UserId private val userId: String,
private val roomAPI: RoomAPI) : SendVerificationMessageTask { private val roomAPI: RoomAPI) : SendVerificationMessageTask {
override fun createParamsAndLocalEcho(type: String, roomId: String, content: Content, cryptoService: CryptoService?): SendVerificationMessageTask.Params {
val localID = LocalEcho.createLocalEchoId()
val event = Event(
roomId = roomId,
originServerTs = System.currentTimeMillis(),
senderId = userId,
eventId = localID,
type = type,
content = content,
unsignedData = UnsignedData(age = null, transactionId = localID)
).also {
localEchoEventFactory.saveLocalEcho(monarchy, it)
}
return SendVerificationMessageTask.Params(
type,
event,
cryptoService
)
}
override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse { override suspend fun execute(params: SendVerificationMessageTask.Params): SendResponse {
val event = createRequestEvent(params) val event = handleEncryption(params)
val localID = event.eventId!! val localID = event.eventId!!
try { try {
@ -57,7 +81,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
val executeRequest = executeRequest<SendResponse> { val executeRequest = executeRequest<SendResponse> {
apiCall = roomAPI.send( apiCall = roomAPI.send(
localID, localID,
roomId = params.roomId, roomId = event.roomId ?: "",
content = event.content, content = event.content,
eventType = event.type eventType = event.type
) )
@ -70,25 +94,12 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
} }
} }
private suspend fun createRequestEvent(params: SendVerificationMessageTask.Params): Event { private suspend fun handleEncryption(params: SendVerificationMessageTask.Params): Event {
val localID = LocalEcho.createLocalEchoId() if (params.cryptoService?.isRoomEncrypted(params.event.roomId ?: "") == true) {
val event = Event(
roomId = params.roomId,
originServerTs = System.currentTimeMillis(),
senderId = userId,
eventId = localID,
type = params.type,
content = params.content,
unsignedData = UnsignedData(age = null, transactionId = localID)
).also {
localEchoEventFactory.saveLocalEcho(monarchy, it)
}
if (params.cryptoService?.isRoomEncrypted(params.roomId) == true) {
try { try {
return encryptEventTask.execute(EncryptEventTask.Params( return encryptEventTask.execute(EncryptEventTask.Params(
params.roomId, params.event.roomId ?: "",
event, params.event,
listOf("m.relates_to"), listOf("m.relates_to"),
params.cryptoService params.cryptoService
)) ))
@ -96,6 +107,6 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
// We said it's ok to send verification request in clear // We said it's ok to send verification request in clear
} }
} }
return event return params.event
} }
} }

View File

@ -38,7 +38,6 @@ import im.vector.matrix.android.internal.crypto.model.MXUsersDevicesMap
import im.vector.matrix.android.internal.crypto.model.rest.* import im.vector.matrix.android.internal.crypto.model.rest.*
import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore import im.vector.matrix.android.internal.crypto.store.IMXCryptoStore
import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask import im.vector.matrix.android.internal.crypto.tasks.DefaultRequestVerificationDMTask
import im.vector.matrix.android.internal.crypto.tasks.RequestVerificationDMTask
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.send.SendResponse import im.vector.matrix.android.internal.session.room.send.SendResponse
import im.vector.matrix.android.internal.task.TaskConstraints import im.vector.matrix.android.internal.task.TaskConstraints
@ -109,6 +108,7 @@ internal class DefaultSasVerificationService @Inject constructor(
onRoomStartRequestReceived(event) onRoomStartRequestReceived(event)
} }
EventType.KEY_VERIFICATION_CANCEL -> { EventType.KEY_VERIFICATION_CANCEL -> {
// MultiSessions | ignore events if i didn't sent the start from this device, or accepted from this device
onRoomCancelReceived(event) onRoomCancelReceived(event)
} }
EventType.KEY_VERIFICATION_ACCEPT -> { EventType.KEY_VERIFICATION_ACCEPT -> {
@ -538,7 +538,7 @@ internal class DefaultSasVerificationService @Inject constructor(
override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) { override fun requestKeyVerificationInDMs(userId: String, roomId: String, callback: MatrixCallback<String>?) {
requestVerificationDMTask.configureWith( requestVerificationDMTask.configureWith(
RequestVerificationDMTask.Params( requestVerificationDMTask.createParamsAndLocalEcho(
roomId = roomId, roomId = roomId,
from = credentials.deviceId ?: "", from = credentials.deviceId ?: "",
methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS), methods = listOf(KeyVerificationStart.VERIF_METHOD_SAS),

View File

@ -50,7 +50,7 @@ internal class SasTransportRoomMessage(
Timber.d("## SAS sending msg type $type") Timber.d("## SAS sending msg type $type")
Timber.v("## SAS sending msg info $verificationInfo") Timber.v("## SAS sending msg info $verificationInfo")
sendVerificationMessageTask.configureWith( sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params( sendVerificationMessageTask.createParamsAndLocalEcho(
type, type,
roomId, roomId,
verificationInfo.toEventContent()!!, verificationInfo.toEventContent()!!,
@ -82,7 +82,7 @@ internal class SasTransportRoomMessage(
override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) { override fun cancelTransaction(transactionId: String, userId: String, userDevice: String, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code") Timber.d("## SAS canceling transaction $transactionId for reason $code")
sendVerificationMessageTask.configureWith( sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params( sendVerificationMessageTask.createParamsAndLocalEcho(
EventType.KEY_VERIFICATION_CANCEL, EventType.KEY_VERIFICATION_CANCEL,
roomId, roomId,
MessageVerificationCancelContent.create(transactionId, code).toContent(), MessageVerificationCancelContent.create(transactionId, code).toContent(),
@ -108,7 +108,7 @@ internal class SasTransportRoomMessage(
override fun done(transactionId: String) { override fun done(transactionId: String) {
sendVerificationMessageTask.configureWith( sendVerificationMessageTask.configureWith(
SendVerificationMessageTask.Params( sendVerificationMessageTask.createParamsAndLocalEcho(
EventType.KEY_VERIFICATION_DONE, EventType.KEY_VERIFICATION_DONE,
roomId, roomId,
MessageVerificationDoneContent( MessageVerificationDoneContent(

View File

@ -19,14 +19,15 @@ import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.crypto.CryptoService import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.crypto.MXCryptoError import im.vector.matrix.android.api.session.crypto.MXCryptoError
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.LocalEcho
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult import im.vector.matrix.android.internal.crypto.algorithms.olm.OlmDecryptionResult
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.mapper.asDomain import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.query.types import im.vector.matrix.android.internal.database.query.types
import im.vector.matrix.android.internal.di.DeviceId
import im.vector.matrix.android.internal.di.SessionDatabase import im.vector.matrix.android.internal.di.SessionDatabase
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
@ -36,13 +37,16 @@ import io.realm.RealmResults
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatabase realmConfiguration: RealmConfiguration, internal class VerificationMessageLiveObserver @Inject constructor(
@UserId private val userId: String, @SessionDatabase realmConfiguration: RealmConfiguration,
private val cryptoService: CryptoService, @UserId private val userId: String,
private val sasVerificationService: DefaultSasVerificationService, @DeviceId private val deviceId: String?,
private val taskExecutor: TaskExecutor) : private val cryptoService: CryptoService,
RealmLiveEntityObserver<EventEntity>(realmConfiguration) { private val sasVerificationService: DefaultSasVerificationService,
private val taskExecutor: TaskExecutor
) : RealmLiveEntityObserver<EventEntity>(realmConfiguration) {
override val query = Monarchy.Query<EventEntity> { override val query = Monarchy.Query<EventEntity> {
EventEntity.types(it, listOf( EventEntity.types(it, listOf(
@ -57,6 +61,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
) )
} }
val transactionsHandledByOtherDevice = ArrayList<String>()
override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) { override fun onChange(results: RealmResults<EventEntity>, changeSet: OrderedCollectionChangeSet) {
// TODO do that in a task // TODO do that in a task
// TODO how to ignore when it's an initial sync? // TODO how to ignore when it's an initial sync?
@ -64,8 +70,8 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
.asSequence() .asSequence()
.mapNotNull { results[it]?.asDomain() } .mapNotNull { results[it]?.asDomain() }
.filterNot { .filterNot {
// ignore mines ^^ // ignore local echos
it.senderId == userId LocalEcho.isLocalEchoId(it.eventId ?: "")
} }
.toList() .toList()
@ -77,7 +83,7 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs val tooInTheFuture = System.currentTimeMillis() + fiveMinInMs
events.forEach { event -> events.forEach { event ->
Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}") Timber.d("## SAS Verification live observer: received msgId: ${event.eventId} msgtype: ${event.type} from ${event.senderId}")
Timber.v("## SAS Verification live observer: received msgId: $event") Timber.v("## SAS Verification live observer: received msgId: $event")
// If the request is in the future by more than 5 minutes or more than 10 minutes in the past, // If the request is in the future by more than 5 minutes or more than 10 minutes in the past,
@ -111,6 +117,45 @@ internal class VerificationMessageLiveObserver @Inject constructor(@SessionDatab
} }
} }
Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}") Timber.v("## SAS Verification live observer: received msgId: ${event.eventId} type: ${event.getClearType()}")
if (event.senderId == userId) {
// If it's send from me, we need to keep track of Requests or Start
// done from another device of mine
if (EventType.MESSAGE == event.type) {
val msgType = event.getClearContent().toModel<MessageContent>()?.type
if (MessageType.MSGTYPE_VERIFICATION_REQUEST == msgType) {
event.getClearContent().toModel<MessageVerificationRequestContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is requested from another device
Timber.v("## SAS Verification live observer: Transaction requested from other device tid:${event.eventId} ")
event.eventId?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
}
} else if (EventType.KEY_VERIFICATION_START == event.type) {
event.getClearContent().toModel<MessageVerificationStartContent>()?.let {
if (it.fromDevice != deviceId) {
// The verification is started from another device
Timber.v("## SAS Verification live observer: Transaction started by other device tid:${it.transactionID} ")
it.transactionID?.let { txId -> transactionsHandledByOtherDevice.add(txId) }
}
}
} else if (EventType.KEY_VERIFICATION_CANCEL == event.type || EventType.KEY_VERIFICATION_DONE == event.type) {
event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId?.let {
transactionsHandledByOtherDevice.remove(it)
}
}
return@forEach
}
val relatesTo = event.getClearContent().toModel<MessageRelationContent>()?.relatesTo?.eventId
if (relatesTo != null && transactionsHandledByOtherDevice.contains(relatesTo)) {
// Ignore this event, it is directed to another of my devices
Timber.v("## SAS Verification live observer: Ignore Transaction handled by other device tid:$relatesTo ")
return@forEach
}
when (event.getClearType()) { when (event.getClearType()) {
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,

View File

@ -25,6 +25,13 @@ import javax.inject.Qualifier
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
internal annotation class UserId internal annotation class UserId
/**
* Used to inject the userId
*/
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
internal annotation class DeviceId
/** /**
* Used to inject the md5 of the userId * Used to inject the md5 of the userId
*/ */

View File

@ -16,19 +16,29 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import im.vector.matrix.android.api.auth.data.Credentials import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import javax.inject.Inject import javax.inject.Inject
internal class AccessTokenInterceptor @Inject constructor(private val credentials: Credentials) : Interceptor { internal class AccessTokenInterceptor @Inject constructor(
@UserId private val userId: String,
private val sessionParamsStore: SessionParamsStore) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request() var request = chain.request()
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set accessToken?.let {
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken) val newRequestBuilder = request.newBuilder()
request = newRequestBuilder.build() // Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
request = newRequestBuilder.build()
}
return chain.proceed(request) return chain.proceed(request)
} }
private val accessToken
get() = sessionParamsStore.get(userId)?.credentials?.accessToken
} }

View File

@ -19,8 +19,8 @@
package im.vector.matrix.android.internal.network package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@ -31,6 +31,7 @@ import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -98,7 +99,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) { if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) { if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// Also send this error to the bus, for a global management // Also send this error to the bus, for a global management
EventBus.getDefault().post(ConsentNotGivenError(matrixError.consentUri)) EventBus.getDefault().post(GlobalError.ConsentNotGivenError(matrixError.consentUri))
} else if (httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& matrixError.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also send this error to the bus, for a global management
EventBus.getDefault().post(GlobalError.InvalidToken(matrixError.isSoftLogout))
} }
return Failure.ServerError(matrixError, httpCode) return Failure.ServerError(matrixError, httpCode)

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.pushrules.PushRuleService import im.vector.matrix.android.api.pushrules.PushRuleService
import im.vector.matrix.android.api.session.InitialSyncProgressService import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.api.session.user.UserService import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.DefaultCryptoService import im.vector.matrix.android.internal.crypto.DefaultCryptoService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.sync.job.SyncWorker import im.vector.matrix.android.internal.session.sync.job.SyncWorker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>, private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>, private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker, private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>, private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>) private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null private var syncThread: SyncThread? = null
override val isOpenable: Boolean
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
@MainThread @MainThread
override fun open() { override fun open() {
assertMainThread() assertMainThread()
@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) { fun onGlobalError(globalError: GlobalError) {
sessionListeners.dispatchConsentNotGiven(consentNotGivenError) if (globalError is GlobalError.InvalidToken
&& globalError.softLogout) {
// Mark the token has invalid
GlobalScope.launch(Dispatchers.IO) {
sessionParamsStore.setTokenInvalid(myUserId)
}
}
sessionListeners.dispatchGlobalError(globalError)
} }
override fun contentUrlResolver() = contentUrlResolver override fun contentUrlResolver() = contentUrlResolver

View File

@ -16,7 +16,7 @@
package im.vector.matrix.android.internal.session package im.vector.matrix.android.internal.session
import im.vector.matrix.android.api.failure.ConsentNotGivenError import im.vector.matrix.android.api.failure.GlobalError
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import javax.inject.Inject import javax.inject.Inject
@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
} }
} }
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) { fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) { synchronized(listeners) {
listeners.forEach { listeners.forEach {
it.onConsentNotGivenError(consentNotGivenError) it.onGlobalError(globalError)
} }
} }
} }

View File

@ -77,6 +77,13 @@ internal abstract class SessionModule {
return credentials.userId return credentials.userId
} }
@JvmStatic
@DeviceId
@Provides
fun providesDeviceId(credentials: Credentials): String? {
return credentials.deviceId
}
@JvmStatic @JvmStatic
@UserMd5 @UserMd5
@Provides @Provides

View File

@ -53,6 +53,9 @@ enum class VerificationState {
DONE DONE
} }
fun VerificationState.isCanceled() : Boolean {
return this == VerificationState.CANCELED_BY_ME || this == VerificationState.CANCELED_BY_OTHER
}
/** /**
* Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base. * Called by EventRelationAggregationUpdater, when new events that can affect relations are inserted in base.
*/ */
@ -433,26 +436,27 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
?: ReferencesAggregatedContent(VerificationState.REQUEST.name) ?: ReferencesAggregatedContent(VerificationState.REQUEST.name)
// TODO ignore invalid messages? e.g a START after a CANCEL? // TODO ignore invalid messages? e.g a START after a CANCEL?
// i.e. never change state if already canceled/done // i.e. never change state if already canceled/done
val currentState = VerificationState.values().firstOrNull { data.verificationSummary == it.name }
val newState = when (event.getClearType()) { val newState = when (event.getClearType()) {
EventType.KEY_VERIFICATION_START -> { EventType.KEY_VERIFICATION_START -> {
VerificationState.WAITING updateVerificationState(currentState, VerificationState.WAITING)
} }
EventType.KEY_VERIFICATION_ACCEPT -> { EventType.KEY_VERIFICATION_ACCEPT -> {
VerificationState.WAITING updateVerificationState(currentState, VerificationState.WAITING)
} }
EventType.KEY_VERIFICATION_KEY -> { EventType.KEY_VERIFICATION_KEY -> {
VerificationState.WAITING updateVerificationState(currentState, VerificationState.WAITING)
} }
EventType.KEY_VERIFICATION_MAC -> { EventType.KEY_VERIFICATION_MAC -> {
VerificationState.WAITING updateVerificationState(currentState, VerificationState.WAITING)
} }
EventType.KEY_VERIFICATION_CANCEL -> { EventType.KEY_VERIFICATION_CANCEL -> {
if (event.senderId == userId) { updateVerificationState(currentState, if (event.senderId == userId) {
VerificationState.CANCELED_BY_ME VerificationState.CANCELED_BY_ME
} else VerificationState.CANCELED_BY_OTHER } else VerificationState.CANCELED_BY_OTHER)
} }
EventType.KEY_VERIFICATION_DONE -> { EventType.KEY_VERIFICATION_DONE -> {
VerificationState.DONE updateVerificationState(currentState, VerificationState.DONE)
} }
else -> VerificationState.REQUEST else -> VerificationState.REQUEST
} }
@ -468,4 +472,18 @@ internal class DefaultEventRelationsAggregationTask @Inject constructor(
verifSummary.sourceEvents.add(event.eventId) verifSummary.sourceEvents.add(event.eventId)
} }
} }
private fun updateVerificationState(oldState: VerificationState?, newState: VerificationState) : VerificationState {
// Cancel is always prioritary ?
// Eg id i found that mac or keys mismatch and send a cancel and the other send a done, i have to
// consider as canceled
if (newState == VerificationState.CANCELED_BY_OTHER || newState == VerificationState.CANCELED_BY_ME) {
return newState
}
// never move out of cancel
if (oldState == VerificationState.CANCELED_BY_OTHER || oldState == VerificationState.CANCELED_BY_ME) {
return oldState
}
return newState
}
} }

View File

@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean { private fun Throwable.shouldBeRetried(): Boolean {
return this is Failure.NetworkConnection return this is Failure.NetworkConnection
|| (this is Failure.ServerError && this.error.code == MatrixError.LIMIT_EXCEEDED) || (this is Failure.ServerError && error.code == MatrixError.M_LIMIT_EXCEEDED)
} }
private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) { private suspend fun sendEvent(eventId: String, eventType: String, content: Content?, roomId: String) {

View File

@ -65,7 +65,7 @@ internal class TextPillsUtils @Inject constructor(
// append text before pill // append text before pill
append(text, currIndex, start) append(text, currIndex, start)
// append the pill // append the pill
append(String.format(template, urlSpan.userId, urlSpan.displayName)) append(String.format(template, urlSpan.matrixItem.id, urlSpan.matrixItem.displayName))
currIndex = end currIndex = end
} }
// append text after the last pill // append text after the last pill

View File

@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.session.signout.SignOutService import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
import im.vector.matrix.android.internal.task.launchToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.GlobalScope
import javax.inject.Inject import javax.inject.Inject
internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask, internal class DefaultSignOutService @Inject constructor(private val signOutTask: SignOutTask,
private val signInAgainTask: SignInAgainTask,
private val sessionParamsStore: SessionParamsStore,
private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val taskExecutor: TaskExecutor) : SignOutService { private val taskExecutor: TaskExecutor) : SignOutService {
override fun signOut(callback: MatrixCallback<Unit>) { override fun signInAgain(password: String,
signOutTask callback: MatrixCallback<Unit>): Cancelable {
.configureWith { return signInAgainTask
.configureWith(SignInAgainTask.Params(password)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
override fun updateCredentials(credentials: Credentials,
callback: MatrixCallback<Unit>): Cancelable {
return GlobalScope.launchToCallback(coroutineDispatchers.main, callback) {
sessionParamsStore.updateCredentials(credentials)
}
}
override fun signOut(sigOutFromHomeserver: Boolean,
callback: MatrixCallback<Unit>): Cancelable {
return signOutTask
.configureWith(SignOutTask.Params(sigOutFromHomeserver)) {
this.callback = callback this.callback = callback
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)

View File

@ -0,0 +1,56 @@
/*
* Copyright 2019 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.matrix.android.internal.session.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
internal interface SignInAgainTask : Task<SignInAgainTask.Params, Unit> {
data class Params(
val password: String
)
}
internal class DefaultSignInAgainTask @Inject constructor(
private val signOutAPI: SignOutAPI,
private val sessionParams: SessionParams,
private val sessionParamsStore: SessionParamsStore) : SignInAgainTask {
override suspend fun execute(params: SignInAgainTask.Params) {
val newCredentials = executeRequest<Credentials> {
apiCall = signOutAPI.loginAgain(
PasswordLoginParams.userIdentifier(
// Reuse the same userId
sessionParams.credentials.userId,
params.password,
// The spec says the initial device name will be ignored
// https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-login
// but https://github.com/matrix-org/synapse/issues/6525
// Reuse the same deviceId
deviceId = sessionParams.credentials.deviceId
)
)
}
sessionParamsStore.updateCredentials(newCredentials)
}
}

View File

@ -16,12 +16,27 @@
package im.vector.matrix.android.internal.session.signout package im.vector.matrix.android.internal.session.signout
import im.vector.matrix.android.api.auth.data.Credentials
import im.vector.matrix.android.internal.auth.data.PasswordLoginParams
import im.vector.matrix.android.internal.network.NetworkConstants import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST import retrofit2.http.POST
internal interface SignOutAPI { internal interface SignOutAPI {
/**
* Attempt to login again to the same account.
* Set all the timeouts to 1 minute
* It is similar to [AuthAPI.login]
*
* @param loginParams the login parameters
*/
@Headers("CONNECT_TIMEOUT:60000", "READ_TIMEOUT:60000", "WRITE_TIMEOUT:60000")
@POST(NetworkConstants.URI_API_PREFIX_PATH_R0 + "login")
fun loginAgain(@Body loginParams: PasswordLoginParams): Call<Credentials>
/** /**
* Invalidate the access token, so that it can no longer be used for authorization. * Invalidate the access token, so that it can no longer be used for authorization.
*/ */

View File

@ -37,8 +37,11 @@ internal abstract class SignOutModule {
} }
@Binds @Binds
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
@Binds @Binds
abstract fun bindSignOutService(signOutService: DefaultSignOutService): SignOutService abstract fun bindSignInAgainTask(task: DefaultSignInAgainTask): SignInAgainTask
@Binds
abstract fun bindSignOutService(service: DefaultSignOutService): SignOutService
} }

View File

@ -18,6 +18,8 @@ package im.vector.matrix.android.internal.session.signout
import android.content.Context import android.content.Context
import im.vector.matrix.android.BuildConfig import im.vector.matrix.android.BuildConfig
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.SessionManager import im.vector.matrix.android.internal.SessionManager
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule import im.vector.matrix.android.internal.crypto.CryptoModule
@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.net.HttpURLConnection
import javax.inject.Inject import javax.inject.Inject
internal interface SignOutTask : Task<Unit, Unit> internal interface SignOutTask : Task<SignOutTask.Params, Unit> {
data class Params(
val sigOutFromHomeserver: Boolean
)
}
internal class DefaultSignOutTask @Inject constructor(private val context: Context, internal class DefaultSignOutTask @Inject constructor(private val context: Context,
@UserId private val userId: String, @UserId private val userId: String,
@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration, @CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask { @UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) { override suspend fun execute(params: SignOutTask.Params) {
Timber.d("SignOut: send request...") // It should be done even after a soft logout, to be sure the deviceId is deleted on the
executeRequest<Unit> { if (params.sigOutFromHomeserver) {
apiCall = signOutAPI.signOut() Timber.d("SignOut: send request...")
try {
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
}
} catch (throwable: Throwable) {
// Maybe due to https://github.com/matrix-org/synapse/issues/5755
if (throwable is Failure.ServerError
&& throwable.httpCode == HttpURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& throwable.error.code == MatrixError.M_UNKNOWN_TOKEN) {
// Also throwable.error.isSoftLogout should be true
// Ignore
Timber.w("Ignore error due to https://github.com/matrix-org/synapse/issues/5755")
} else {
throw throwable
}
}
} }
Timber.d("SignOut: release session...") Timber.d("SignOut: release session...")

View File

@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync package im.vector.matrix.android.internal.session.sync
import im.vector.matrix.android.R import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.auth.SessionParamsStore import im.vector.matrix.android.internal.auth.SessionParamsStore
import im.vector.matrix.android.internal.di.UserId import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest import im.vector.matrix.android.internal.network.executeRequest
@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll() initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100) initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
} }
val syncResponse = try { val syncResponse = executeRequest<SyncResponse> {
executeRequest<SyncResponse> { apiCall = syncAPI.sync(requestParams)
apiCall = syncAPI.sync(requestParams)
}
} catch (throwable: Throwable) {
// Intercept 401
if (throwable is Failure.ServerError
&& throwable.error.code == MatrixError.UNKNOWN_TOKEN) {
sessionParamsStore.delete(userId)
}
throw throwable
} }
syncResponseHandler.handleResponse(syncResponse, token) syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch) syncTokenStore.saveToken(syncResponse.nextBatch)

View File

@ -147,7 +147,7 @@ open class SyncService : Service() {
} }
if (failure is Failure.ServerError if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token, stop the thread
stopSelf() stopSelf()
} }

View File

@ -44,19 +44,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val taskExecutor: TaskExecutor private val taskExecutor: TaskExecutor
) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener { ) : Thread(), NetworkConnectivityChecker.Listener, BackgroundDetectionObserver.Listener {
private var state: SyncState = SyncState.IDLE private var state: SyncState = SyncState.Idle
private var liveState = MutableLiveData<SyncState>() private var liveState = MutableLiveData<SyncState>()
private val lock = Object() private val lock = Object()
private var cancelableTask: Cancelable? = null private var cancelableTask: Cancelable? = null
private var isStarted = false private var isStarted = false
private var isTokenValid = true
init { init {
updateStateTo(SyncState.IDLE) updateStateTo(SyncState.Idle)
} }
fun setInitialForeground(initialForeground: Boolean) { fun setInitialForeground(initialForeground: Boolean) {
val newState = if (initialForeground) SyncState.IDLE else SyncState.PAUSED val newState = if (initialForeground) SyncState.Idle else SyncState.Paused
updateStateTo(newState) updateStateTo(newState)
} }
@ -64,6 +65,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
if (!isStarted) { if (!isStarted) {
Timber.v("Resume sync...") Timber.v("Resume sync...")
isStarted = true isStarted = true
// Check again the token validity
isTokenValid = true
lock.notify() lock.notify()
} }
} }
@ -78,7 +81,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
fun kill() = synchronized(lock) { fun kill() = synchronized(lock) {
Timber.v("Kill sync...") Timber.v("Kill sync...")
updateStateTo(SyncState.KILLING) updateStateTo(SyncState.Killing)
cancelableTask?.cancel() cancelableTask?.cancel()
lock.notify() lock.notify()
} }
@ -100,26 +103,31 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
networkConnectivityChecker.register(this) networkConnectivityChecker.register(this)
backgroundDetectionObserver.register(this) backgroundDetectionObserver.register(this)
while (state != SyncState.KILLING) { while (state != SyncState.Killing) {
Timber.v("Entering loop, state: $state") Timber.v("Entering loop, state: $state")
if (!networkConnectivityChecker.hasInternetAccess) { if (!networkConnectivityChecker.hasInternetAccess) {
Timber.v("No network. Waiting...") Timber.v("No network. Waiting...")
updateStateTo(SyncState.NO_NETWORK) updateStateTo(SyncState.NoNetwork)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else if (!isStarted) { } else if (!isStarted) {
Timber.v("Sync is Paused. Waiting...") Timber.v("Sync is Paused. Waiting...")
updateStateTo(SyncState.PAUSED) updateStateTo(SyncState.Paused)
synchronized(lock) { lock.wait() }
Timber.v("...unlocked")
} else if (!isTokenValid) {
Timber.v("Token is invalid. Waiting...")
updateStateTo(SyncState.InvalidToken)
synchronized(lock) { lock.wait() } synchronized(lock) { lock.wait() }
Timber.v("...unlocked") Timber.v("...unlocked")
} else { } else {
if (state !is SyncState.RUNNING) { if (state !is SyncState.Running) {
updateStateTo(SyncState.RUNNING(afterPause = true)) updateStateTo(SyncState.Running(afterPause = true))
} }
// No timeout after a pause // No timeout after a pause
val timeout = state.let { if (it is SyncState.RUNNING && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT } val timeout = state.let { if (it is SyncState.Running && it.afterPause) 0 else DEFAULT_LONG_POOL_TIMEOUT }
Timber.v("Execute sync request with timeout $timeout") Timber.v("Execute sync request with timeout $timeout")
val latch = CountDownLatch(1) val latch = CountDownLatch(1)
@ -141,10 +149,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} else if (failure is Failure.Cancelled) { } else if (failure is Failure.Cancelled) {
Timber.v("Cancelled") Timber.v("Cancelled")
} else if (failure is Failure.ServerError } else if (failure is Failure.ServerError
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) { && (failure.error.code == MatrixError.M_UNKNOWN_TOKEN || failure.error.code == MatrixError.M_MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token
Timber.w(failure) Timber.w(failure)
updateStateTo(SyncState.KILLING) isTokenValid = false
isStarted = false
} else { } else {
Timber.e(failure) Timber.e(failure)
@ -163,8 +172,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
latch.await() latch.await()
state.let { state.let {
if (it is SyncState.RUNNING && it.afterPause) { if (it is SyncState.Running && it.afterPause) {
updateStateTo(SyncState.RUNNING(afterPause = false)) updateStateTo(SyncState.Running(afterPause = false))
} }
} }
@ -172,7 +181,7 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} }
} }
Timber.v("Sync killed") Timber.v("Sync killed")
updateStateTo(SyncState.KILLED) updateStateTo(SyncState.Killed)
backgroundDetectionObserver.unregister(this) backgroundDetectionObserver.unregister(this)
networkConnectivityChecker.unregister(this) networkConnectivityChecker.unregister(this)
} }

View File

@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.util package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber import timber.log.Timber
import java.util.Locale
/** /**
* Convert a string to an UTF8 String * Convert a string to an UTF8 String
@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
s s
} }
} }
fun String?.firstLetterOfDisplayName(): String {
if (this.isNullOrEmpty()) return ""
val isUserId = MatrixPatterns.isUserId(this)
val firstLetterIndex = if (isUserId) 1 else 0
return this[firstLetterIndex].toString().toUpperCase(Locale.ROOT)
}

View File

@ -1 +1 @@
include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx' include ':vector', ':matrix-sdk-android', ':matrix-sdk-android-rx', ':diff-match-patch'

View File

@ -229,6 +229,7 @@ dependencies {
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch")
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -341,8 +342,6 @@ dependencies {
exclude group: 'com.google.firebase', module: 'firebase-measurement-connector' exclude group: 'com.google.firebase', module: 'firebase-measurement-connector'
} }
implementation 'diff_match_patch:diff_match_patch:current'
implementation "androidx.emoji:emoji-appcompat:1.0.0" implementation "androidx.emoji:emoji-appcompat:1.0.0"
// TESTS // TESTS

View File

@ -98,6 +98,10 @@
<category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.OPENABLE" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".features.signout.hard.SignedOutActivity" />
<activity
android:name=".features.signout.soft.SoftLogoutActivity"
android:windowSoftInputMode="adjustResize" />
<!-- Services --> <!-- Services -->
<service <service

View File

@ -37,7 +37,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
fun setActiveSession(session: Session) { fun setActiveSession(session: Session) {
activeSession.set(session) activeSession.set(session)
sessionObservableStore.post(Option.fromNullable(session)) sessionObservableStore.post(Option.just(session))
keyRequestHandler.start(session) keyRequestHandler.start(session)
incomingVerificationRequestHandler.start(session) incomingVerificationRequestHandler.start(session)
} }

View File

@ -47,6 +47,7 @@ import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFr
import im.vector.riotx.features.settings.* import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment import im.vector.riotx.features.settings.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@Module @Module
interface FragmentModule { interface FragmentModule {
@ -266,4 +267,9 @@ interface FragmentModule {
@IntoMap @IntoMap
@FragmentKey(EmojiChooserFragment::class) @FragmentKey(EmojiChooserFragment::class)
fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment fun bindEmojiChooserFragment(fragment: EmojiChooserFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SoftLogoutFragment::class)
fun bindSoftLogoutFragment(fragment: SoftLogoutFragment): Fragment
} }

View File

@ -21,6 +21,7 @@ import androidx.fragment.app.FragmentFactory
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import dagger.BindsInstance import dagger.BindsInstance
import dagger.Component import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity import im.vector.riotx.features.crypto.keysbackup.settings.KeysBackupManageActivity
@ -49,6 +50,7 @@ import im.vector.riotx.features.roomdirectory.RoomDirectoryActivity
import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity import im.vector.riotx.features.roomdirectory.createroom.CreateRoomActivity
import im.vector.riotx.features.settings.VectorSettingsActivity import im.vector.riotx.features.settings.VectorSettingsActivity
import im.vector.riotx.features.share.IncomingShareActivity import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository import im.vector.riotx.features.ui.UiStateRepository
@Component( @Component(
@ -78,6 +80,8 @@ interface ScreenComponent {
fun navigator(): Navigator fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity) fun inject(activity: HomeActivity)
@ -126,6 +130,8 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
@Component.Factory @Component.Factory
interface Factory { interface Factory {
fun create(vectorComponent: VectorComponent, fun create(vectorComponent: VectorComponent,

View File

@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication import im.vector.riotx.VectorApplication
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter import im.vector.riotx.core.utils.DimensionConverter
@ -88,6 +89,8 @@ interface VectorComponent {
fun navigator(): Navigator fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun homeRoomListObservableStore(): HomeRoomListDataSource fun homeRoomListObservableStore(): HomeRoomListDataSource
fun shareRoomListObservableStore(): ShareRoomListDataSource fun shareRoomListObservableStore(): ShareRoomListDataSource

View File

@ -26,6 +26,8 @@ import dagger.Provides
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.auth.AuthenticationService import im.vector.matrix.android.api.auth.AuthenticationService
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.error.DefaultErrorFormatter
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.DefaultNavigator import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
@ -72,6 +74,9 @@ abstract class VectorModule {
@Binds @Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter
@Binds @Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
} }

View File

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -37,11 +38,7 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarUrl: String lateinit var matrixItem: MatrixItem
@EpoxyAttribute
lateinit var senderId: String
@EpoxyAttribute
var senderName: String? = null
@EpoxyAttribute @EpoxyAttribute
lateinit var body: CharSequence lateinit var body: CharSequence
@EpoxyAttribute @EpoxyAttribute
@ -50,8 +47,8 @@ abstract class BottomSheetMessagePreviewItem : VectorEpoxyModel<BottomSheetMessa
var movementMethod: MovementMethod? = null var movementMethod: MovementMethod? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, senderId, senderName, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
holder.sender.setTextOrHide(senderName) holder.sender.setTextOrHide(matrixItem.displayName)
holder.body.movementMethod = movementMethod holder.body.movementMethod = movementMethod
holder.body.text = body holder.body.text = body
body.findPillsAndProcess { it.bind(holder.body) } body.findPillsAndProcess { it.bind(holder.body) }

View File

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -36,16 +37,12 @@ abstract class BottomSheetRoomPreviewItem : VectorEpoxyModel<BottomSheetRoomPrev
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarRenderer: AvatarRenderer lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute @EpoxyAttribute
lateinit var avatarUrl: String lateinit var matrixItem: MatrixItem
@EpoxyAttribute
lateinit var roomId: String
@EpoxyAttribute
var roomName: String? = null
@EpoxyAttribute var settingsClickListener: View.OnClickListener? = null @EpoxyAttribute var settingsClickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, roomId, roomName, holder.avatar) avatarRenderer.render(matrixItem, holder.avatar)
holder.roomName.setTextOrHide(roomName) holder.roomName.setTextOrHide(matrixItem.displayName)
holder.roomSettings.setOnClickListener(settingsClickListener) holder.roomSettings.setOnClickListener(settingsClickListener)
} }

View File

@ -25,14 +25,15 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException import java.net.UnknownHostException
import javax.inject.Inject import javax.inject.Inject
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) { interface ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String
}
fun toHumanReadable(failure: Failure): String { class DefaultErrorFormatter @Inject constructor(
// Default private val stringProvider: StringProvider
return failure.localizedMessage ) : ErrorFormatter {
}
fun toHumanReadable(throwable: Throwable?): String { override fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) { return when (throwable) {
null -> null null -> null
is Failure.NetworkConnection -> { is Failure.NetworkConnection -> {
@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout) stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException -> throwable.ioException is UnknownHostException ->
// Invalid homeserver? // Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host) stringProvider.getString(R.string.login_error_unknown_host)
else -> else ->
stringProvider.getString(R.string.error_no_network) stringProvider.getString(R.string.error_no_network)
@ -52,23 +54,23 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
// Special case for terms and conditions // Special case for terms and conditions
stringProvider.getString(R.string.error_terms_not_accepted) stringProvider.getString(R.string.error_terms_not_accepted)
} }
throwable.error.code == MatrixError.FORBIDDEN throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> { && throwable.error.message == "Invalid password" -> {
stringProvider.getString(R.string.auth_invalid_login_param) stringProvider.getString(R.string.auth_invalid_login_param)
} }
throwable.error.code == MatrixError.USER_IN_USE -> { throwable.error.code == MatrixError.M_USER_IN_USE -> {
stringProvider.getString(R.string.login_signup_error_user_in_use) stringProvider.getString(R.string.login_signup_error_user_in_use)
} }
throwable.error.code == MatrixError.BAD_JSON -> { throwable.error.code == MatrixError.M_BAD_JSON -> {
stringProvider.getString(R.string.login_error_bad_json) stringProvider.getString(R.string.login_error_bad_json)
} }
throwable.error.code == MatrixError.NOT_JSON -> { throwable.error.code == MatrixError.M_NOT_JSON -> {
stringProvider.getString(R.string.login_error_not_json) stringProvider.getString(R.string.login_error_not_json)
} }
throwable.error.code == MatrixError.LIMIT_EXCEEDED -> { throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
limitExceededError(throwable.error) limitExceededError(throwable.error)
} }
throwable.error.code == MatrixError.THREEPID_NOT_FOUND -> { throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
stringProvider.getString(R.string.login_reset_password_error_not_found) stringProvider.getString(R.string.login_reset_password_error_not_found)
} }
else -> { else -> {

View File

@ -21,6 +21,6 @@ import im.vector.matrix.android.api.failure.MatrixError
import javax.net.ssl.HttpsURLConnection import javax.net.ssl.HttpsURLConnection
fun Throwable.is401(): Boolean { fun Throwable.is401(): Boolean {
return (this is Failure.ServerError && this.httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */ return (this is Failure.ServerError && httpCode == HttpsURLConnection.HTTP_UNAUTHORIZED /* 401 */
&& this.error.code == MatrixError.UNAUTHORIZED) && error.code == MatrixError.M_UNAUTHORIZED)
} }

View File

@ -19,6 +19,7 @@ package im.vector.riotx.core.extensions
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.sync.FilterService import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
import im.vector.riotx.features.session.SessionListener import im.vector.riotx.features.session.SessionListener
@ -40,3 +41,11 @@ fun Session.configureAndStart(pushRuleTriggerListener: PushRuleTriggerListener,
// @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler // @Inject lateinit var incomingVerificationRequestHandler: IncomingVerificationRequestHandler
// @Inject lateinit var keyRequestHandler: KeyRequestHandler // @Inject lateinit var keyRequestHandler: KeyRequestHandler
} }
/**
* Tell is the session has unsaved e2e keys in the backup
*/
fun Session.hasUnsavedKeys(): Boolean {
return inboundGroupSessionsCount(false) > 0
&& getKeysBackupService().state != KeysBackupState.ReadyToBackUp
}

View File

@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder
return this return this
} }
/**
* Ex: "https://matrix.org/" -> "matrix.org"
*/
fun String?.toReducedUrl(): String {
return (this ?: "")
.substringAfter("://")
.trim { it == '/' }
}

View File

@ -38,12 +38,15 @@ import butterknife.Unbinder
import com.airbnb.mvrx.MvRx import com.airbnb.mvrx.MvRx
import com.bumptech.glide.util.Util import com.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.BuildConfig import im.vector.riotx.BuildConfig
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.* import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.extensions.observeEvent import im.vector.riotx.core.extensions.observeEvent
import im.vector.riotx.core.utils.toast import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.MainActivity
import im.vector.riotx.features.MainActivityArgs
import im.vector.riotx.features.configuration.VectorConfiguration import im.vector.riotx.features.configuration.VectorConfiguration
import im.vector.riotx.features.consent.ConsentNotGivenHelper import im.vector.riotx.features.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected lateinit var navigator: Navigator protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder private lateinit var activeSessionHolder: ActiveSessionHolder
// Filter for multiple invalid token error
private var mainActivityStarted = false
private var unBinder: Unbinder? = null private var unBinder: Unbinder? = null
private var savedInstanceState: Bundle? = null private var savedInstanceState: Bundle? = null
@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
}) })
sessionListener = getVectorComponent().sessionListener() sessionListener = getVectorComponent().sessionListener()
sessionListener.consentNotGivenLiveData.observeEvent(this) { sessionListener.globalErrorLiveData.observeEvent(this) {
consentNotGivenHelper.displayDialog(it.consentUri, handleGlobalError(it)
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
} }
doBeforeSetContentView() doBeforeSetContentView()
@ -180,6 +185,33 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
} }
} }
private fun handleGlobalError(globalError: GlobalError) {
when (globalError) {
is GlobalError.InvalidToken ->
handleInvalidToken(globalError)
is GlobalError.ConsentNotGivenError ->
consentNotGivenHelper.displayDialog(globalError.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
}
}
protected open fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
Timber.w("Invalid token event received")
if (mainActivityStarted) {
return
}
mainActivityStarted = true
MainActivity.restartApp(this,
MainActivityArgs(
clearCredentials = !globalError.softLogout,
isUserLoggedOut = true,
isSoftLogout = globalError.softLogout
)
)
}
override fun onDestroy() { override fun onDestroy() {
super.onDestroy() super.onDestroy()
unBinder?.unbind() unBinder?.unbind()

View File

@ -34,6 +34,7 @@ import com.bumptech.glide.util.Util.assertMainThread
import im.vector.riotx.core.di.DaggerScreenComponent import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.HasScreenInjector import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -49,12 +50,14 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
} }
/* ========================================================================================== /* ==========================================================================================
* Navigator * Navigator and other common objects
* ========================================================================================== */ * ========================================================================================== */
protected lateinit var navigator: Navigator
private lateinit var screenComponent: ScreenComponent private lateinit var screenComponent: ScreenComponent
protected lateinit var navigator: Navigator
protected lateinit var errorFormatter: ErrorFormatter
/* ========================================================================================== /* ==========================================================================================
* View model * View model
* ========================================================================================== */ * ========================================================================================== */
@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity) screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator() navigator = screenComponent.navigator()
errorFormatter = screenComponent.errorFormatter()
viewModelFactory = screenComponent.viewModelFactory() viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory() childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector()) injectWith(injector())

View File

@ -23,6 +23,8 @@ import android.widget.ProgressBar
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceViewHolder import androidx.preference.PreferenceViewHolder
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.vectorComponent import im.vector.riotx.core.extensions.vectorComponent
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -59,9 +61,9 @@ open class UserAvatarPreference : Preference {
val session = mSession ?: return val session = mSession ?: return
val view = mAvatarView ?: return val view = mAvatarView ?: return
session.getUser(session.myUserId)?.let { session.getUser(session.myUserId)?.let {
avatarRenderer.render(it, view) avatarRenderer.render(it.toMatrixItem(), view)
} ?: run { } ?: run {
avatarRenderer.render(null, session.myUserId, null, view) avatarRenderer.render(MatrixItem.UserItem(session.myUserId), view)
} }
} }

View File

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem
import kotlinx.android.synthetic.main.view_read_receipts.view.* import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5 private const val MAX_RECEIPT_DISPLAYED = 5
@ -59,7 +60,7 @@ class ReadReceiptsView @JvmOverloads constructor(
receiptAvatars[index].visibility = View.INVISIBLE receiptAvatars[index].visibility = View.INVISIBLE
} else { } else {
receiptAvatars[index].visibility = View.VISIBLE receiptAvatars[index].visibility = View.VISIBLE
avatarRenderer.render(receiptData.avatarUrl, receiptData.userId, receiptData.displayName, receiptAvatars[index]) avatarRenderer.render(receiptData.toMatrixItem(), receiptAvatars[index])
} }
} }

View File

@ -19,9 +19,11 @@ package im.vector.riotx.features
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent import im.vector.riotx.core.di.ScreenComponent
@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity import im.vector.riotx.features.home.HomeActivity
import im.vector.riotx.features.login.LoginActivity import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.notifications.NotificationDrawerManager
import im.vector.riotx.features.signout.hard.SignedOutActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@Parcelize
data class MainActivityArgs(
val clearCache: Boolean = false,
val clearCredentials: Boolean = false,
val isUserLoggedOut: Boolean = false,
val isSoftLogout: Boolean = false
) : Parcelable
/**
* This is the entry point of RiotX
* This Activity, when started with argument, is also doing some cleanup when user disconnects,
* clears cache, is logged out, or is soft logged out
*/
class MainActivity : VectorBaseActivity() { class MainActivity : VectorBaseActivity() {
companion object { companion object {
private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE" private const val EXTRA_ARGS = "EXTRA_ARGS"
private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS"
// Special action to clear cache and/or clear credentials // Special action to clear cache and/or clear credentials
fun restartApp(activity: Activity, clearCache: Boolean = false, clearCredentials: Boolean = false) { fun restartApp(activity: Activity, args: MainActivityArgs) {
val intent = Intent(activity, MainActivity::class.java) val intent = Intent(activity, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra(EXTRA_CLEAR_CACHE, clearCache) intent.putExtra(EXTRA_ARGS, args)
intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials)
activity.startActivity(intent) activity.startActivity(intent)
} }
} }
private lateinit var args: MainActivityArgs
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder @Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter @Inject lateinit var errorFormatter: ErrorFormatter
@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false) args = parseArgs()
val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false)
if (args.clearCredentials || args.isUserLoggedOut) {
clearNotifications()
}
// Handle some wanted cleanup // Handle some wanted cleanup
if (clearCache || clearCredentials) { if (args.clearCache || args.clearCredentials) {
doCleanUp(clearCache, clearCredentials) doCleanUp()
} else { } else {
start() startNextActivityAndFinish()
} }
} }
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) { private fun clearNotifications() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()
notificationDrawerManager.persistInfo()
}
private fun parseArgs(): MainActivityArgs {
val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS)
Timber.w("Starting MainActivity with $argsFromIntent")
return MainActivityArgs(
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
private fun doCleanUp() {
when { when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> { args.clearCredentials -> sessionHolder.getActiveSession().signOut(
override fun onSuccess(data: Unit) { !args.isUserLoggedOut,
Timber.w("SIGN_OUT: success, start app") object : MatrixCallback<Unit> {
sessionHolder.clearActiveSession() override fun onSuccess(data: Unit) {
doLocalCleanupAndStart() Timber.w("SIGN_OUT: success, start app")
} sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials) displayError(failure)
} }
}) })
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> { args.clearCache -> sessionHolder.getActiveSession().clearCache(
override fun onSuccess(data: Unit) { object : MatrixCallback<Unit> {
doLocalCleanupAndStart() override fun onSuccess(data: Unit) {
} doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) { override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials) displayError(failure)
} }
}) })
} }
} }
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanupAndStart() { private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) { GlobalScope.launch(Dispatchers.Main) {
// On UI Thread // On UI Thread
@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() {
} }
} }
start() startNextActivityAndFinish()
} }
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) { private fun displayError(failure: Throwable) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error) .setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure)) .setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) } .setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setNegativeButton(R.string.cancel) { _, _ -> start() } .setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
.setCancelable(false) .setCancelable(false)
.show() .show()
} }
private fun start() { private fun startNextActivityAndFinish() {
val intent = if (sessionHolder.hasActiveSession()) { val intent = when {
HomeActivity.newIntent(this) args.clearCredentials
} else { && !args.isUserLoggedOut ->
LoginActivity.newIntent(this, null) // User has explicitly asked to log out
LoginActivity.newIntent(this, null)
args.isSoftLogout ->
// The homeserver has invalidated the token, with a soft logout
SoftLogoutActivity.newIntent(this)
args.isUserLoggedOut ->
// the homeserver has invalidated the token (password changed, device deleted, other security reason
SignedOutActivity.newIntent(this)
sessionHolder.hasActiveSession() ->
// We have a session.
// Check it can be opened
if (sessionHolder.getActiveSession().isOpenable) {
HomeActivity.newIntent(this)
} else {
// The token is still invalid
SoftLogoutActivity.newIntent(this)
}
else ->
// First start, or no active session
LoginActivity.newIntent(this, null)
} }
startActivity(intent) startActivity(intent)
finish() finish()

View File

@ -18,11 +18,12 @@ package im.vector.riotx.features.autocomplete.user
import com.airbnb.epoxy.TypedEpoxyController import com.airbnb.epoxy.TypedEpoxyController
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.autocomplete.AutocompleteClickListener import im.vector.riotx.features.autocomplete.AutocompleteClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
class AutocompleteUserController @Inject constructor(): TypedEpoxyController<List<User>>() { class AutocompleteUserController @Inject constructor() : TypedEpoxyController<List<User>>() {
var listener: AutocompleteClickListener<User>? = null var listener: AutocompleteClickListener<User>? = null
@ -35,9 +36,7 @@ class AutocompleteUserController @Inject constructor(): TypedEpoxyController<Lis
data.forEach { user -> data.forEach { user ->
autocompleteUserItem { autocompleteUserItem {
id(user.userId) id(user.userId)
userId(user.userId) matrixItem(user.toMatrixItem())
name(user.displayName)
avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
clickListener { _ -> clickListener { _ ->
listener?.onItemClick(user) listener?.onItemClick(user)

View File

@ -21,6 +21,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -30,15 +31,13 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() { abstract class AutocompleteUserItem : VectorEpoxyModel<AutocompleteUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var name: String? = null @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
holder.nameView.text = name holder.nameView.text = matrixItem.getBestName()
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -22,6 +22,8 @@ import androidx.lifecycle.Observer
import butterknife.BindView import butterknife.BindView
import butterknife.OnClick import butterknife.OnClick
import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction import im.vector.matrix.android.api.session.crypto.sas.IncomingSasVerificationTransaction
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.platform.VectorBaseFragment import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -57,10 +59,10 @@ class SASVerificationIncomingFragment @Inject constructor(
otherDeviceTextView.text = viewModel.otherDeviceId otherDeviceTextView.text = viewModel.otherDeviceId
viewModel.otherUser?.let { viewModel.otherUser?.let {
avatarRenderer.render(it, avatarImageView) avatarRenderer.render(it.toMatrixItem(), avatarImageView)
} ?: run { } ?: run {
// Fallback to what we know // Fallback to what we know
avatarRenderer.render(null, viewModel.otherUserId ?: "", viewModel.otherUserId, avatarImageView) avatarRenderer.render(MatrixItem.UserItem(viewModel.otherUserId ?: "", viewModel.otherUserId), avatarImageView)
} }
viewModel.transactionState.observe(viewLifecycleOwner, Observer { viewModel.transactionState.observe(viewLifecycleOwner, Observer {

View File

@ -27,10 +27,7 @@ import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.DrawableImageViewTarget import com.bumptech.glide.request.target.DrawableImageViewTarget
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest import im.vector.riotx.core.glide.GlideRequest
@ -45,76 +42,42 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
companion object { companion object {
private const val THUMBNAIL_SIZE = 250 private const val THUMBNAIL_SIZE = 250
private val AVATAR_COLOR_LIST = listOf(
R.color.riotx_avatar_fill_1,
R.color.riotx_avatar_fill_2,
R.color.riotx_avatar_fill_3
)
} }
@UiThread @UiThread
fun render(roomSummary: RoomSummary, imageView: ImageView) { fun render(matrixItem: MatrixItem, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView) render(imageView.context,
} GlideApp.with(imageView),
matrixItem,
@UiThread DrawableImageViewTarget(imageView))
fun render(user: User, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), user.avatarUrl, user.userId, user.displayName, DrawableImageViewTarget(imageView))
}
@UiThread
fun render(avatarUrl: String?, identifier: String, name: String?, imageView: ImageView) {
render(imageView.context, GlideApp.with(imageView), avatarUrl, identifier, name, DrawableImageViewTarget(imageView))
} }
@UiThread @UiThread
fun render(context: Context, fun render(context: Context,
glideRequest: GlideRequests, glideRequest: GlideRequests,
avatarUrl: String?, matrixItem: MatrixItem,
identifier: String,
name: String?,
target: Target<Drawable>) { target: Target<Drawable>) {
val displayName = if (name.isNullOrBlank()) { val placeholder = getPlaceholderDrawable(context, matrixItem)
identifier buildGlideRequest(glideRequest, matrixItem.avatarUrl)
} else {
name
}
val placeholder = getPlaceholderDrawable(context, identifier, displayName)
buildGlideRequest(glideRequest, avatarUrl)
.placeholder(placeholder) .placeholder(placeholder)
.into(target) .into(target)
} }
@AnyThread @AnyThread
fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable { fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
val avatarColor = ContextCompat.getColor(context, getColorFromUserId(identifier)) val avatarColor = when (matrixItem) {
return if (text.isEmpty()) { is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id))
TextDrawable.builder().buildRound("", avatarColor) else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id))
} else {
val firstLetter = text.firstLetterOfDisplayName()
TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(firstLetter, avatarColor)
} }
return TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
} }
// PRIVATE API ********************************************************************************* // PRIVATE API *********************************************************************************
// private fun getAvatarColor(text: String? = null): Int {
// var colorIndex: Long = 0
// if (!text.isNullOrEmpty()) {
// var sum: Long = 0
// for (i in 0 until text.length) {
// sum += text[i].toLong()
// }
// colorIndex = sum % AVATAR_COLOR_LIST.size
// }
// return AVATAR_COLOR_LIST[colorIndex.toInt()]
// }
private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> { private fun buildGlideRequest(glideRequest: GlideRequests, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver() val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
.resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE) .resolveThumbnail(avatarUrl, THUMBNAIL_SIZE, THUMBNAIL_SIZE, ContentUrlResolver.ThumbnailMethod.SCALE)

View File

@ -27,6 +27,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationItemView
import com.google.android.material.bottomnavigation.BottomNavigationMenuView import com.google.android.material.bottomnavigation.BottomNavigationMenuView
import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState import im.vector.matrix.android.api.session.crypto.keysbackup.KeysBackupState
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.platform.ToolbarConfigurable import im.vector.riotx.core.platform.ToolbarConfigurable
@ -74,12 +75,7 @@ class HomeDetailFragment @Inject constructor(
private fun onGroupChange(groupSummary: GroupSummary?) { private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let { groupSummary?.let {
avatarRenderer.render( avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView)
it.avatarUrl,
it.groupId,
it.displayName,
groupToolbarAvatarImageView
)
} }
} }
@ -155,7 +151,7 @@ class HomeDetailFragment @Inject constructor(
bottomNavigationView.selectedItemId = when (displayMode) { bottomNavigationView.selectedItemId = when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home else -> R.id.bottom_action_home
} }
} }

View File

@ -34,5 +34,5 @@ data class HomeDetailViewState(
val notificationHighlightPeople: Boolean = false, val notificationHighlightPeople: Boolean = false,
val notificationCountRooms: Int = 0, val notificationCountRooms: Int = 0,
val notificationHighlightRooms: Boolean = false, val notificationHighlightRooms: Boolean = false,
val syncState: SyncState = SyncState.IDLE val syncState: SyncState = SyncState.Idle
) : MvRxState ) : MvRxState

View File

@ -19,6 +19,7 @@ package im.vector.riotx.features.home
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.extensions.observeK import im.vector.riotx.core.extensions.observeK
import im.vector.riotx.core.extensions.replaceChildFragment import im.vector.riotx.core.extensions.replaceChildFragment
@ -42,7 +43,7 @@ class HomeDrawerFragment @Inject constructor(
session.liveUser(session.myUserId).observeK(this) { optionalUser -> session.liveUser(session.myUserId).observeK(this) { optionalUser ->
val user = optionalUser?.getOrNull() val user = optionalUser?.getOrNull()
if (user != null) { if (user != null) {
avatarRenderer.render(user.avatarUrl, user.userId, user.displayName, homeDrawerHeaderAvatarView) avatarRenderer.render(user.toMatrixItem(), homeDrawerHeaderAvatarView)
homeDrawerUsernameView.text = user.displayName homeDrawerUsernameView.text = user.displayName
homeDrawerUserIdView.text = user.userId homeDrawerUserIdView.text = user.userId
} }

View File

@ -0,0 +1,29 @@
/*
* Copyright 2019 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.riotx.features.home
import androidx.annotation.ColorRes
import im.vector.riotx.R
@ColorRes
fun getColorFromRoomId(roomId: String?): Int {
return when ((roomId?.toList()?.sumBy { it.toInt() } ?: 0) % 3) {
1 -> R.color.riotx_avatar_fill_2
2 -> R.color.riotx_avatar_fill_3
else -> R.color.riotx_avatar_fill_1
}
}

View File

@ -22,28 +22,18 @@ import kotlin.math.abs
@ColorRes @ColorRes
fun getColorFromUserId(userId: String?): Int { fun getColorFromUserId(userId: String?): Int {
if (userId.isNullOrBlank()) {
return R.color.riotx_username_1
}
var hash = 0 var hash = 0
var i = 0
var chr: Char
while (i < userId.length) { userId?.toList()?.map { chr -> hash = (hash shl 5) - hash + chr.toInt() }
chr = userId[i]
hash = (hash shl 5) - hash + chr.toInt()
i++
}
return when (abs(hash) % 8 + 1) { return when (abs(hash) % 8) {
1 -> R.color.riotx_username_1 1 -> R.color.riotx_username_2
2 -> R.color.riotx_username_2 2 -> R.color.riotx_username_3
3 -> R.color.riotx_username_3 3 -> R.color.riotx_username_4
4 -> R.color.riotx_username_4 4 -> R.color.riotx_username_5
5 -> R.color.riotx_username_5 5 -> R.color.riotx_username_6
6 -> R.color.riotx_username_6 6 -> R.color.riotx_username_7
7 -> R.color.riotx_username_7 7 -> R.color.riotx_username_8
else -> R.color.riotx_username_8 else -> R.color.riotx_username_1
} }
} }

View File

@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -34,22 +35,20 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() { abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute var name: String? = null @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var clickListener: View.OnClickListener? = null @EpoxyAttribute var clickListener: View.OnClickListener? = null
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
holder.view.setOnClickListener(clickListener) holder.view.setOnClickListener(clickListener)
// If name is empty, use userId as name and force it being centered // If name is empty, use userId as name and force it being centered
if (name.isNullOrEmpty()) { if (matrixItem.displayName.isNullOrEmpty()) {
holder.userIdView.visibility = View.GONE holder.userIdView.visibility = View.GONE
holder.nameView.text = userId holder.nameView.text = matrixItem.id
} else { } else {
holder.userIdView.visibility = View.VISIBLE holder.userIdView.visibility = View.VISIBLE
holder.nameView.text = name holder.nameView.text = matrixItem.displayName
holder.userIdView.text = userId holder.userIdView.text = matrixItem.id
} }
renderSelection(holder, selected) renderSelection(holder, selected)
} }
@ -62,7 +61,7 @@ abstract class CreateDirectRoomUserItem : VectorEpoxyModel<CreateDirectRoomUserI
holder.avatarImageView.setImageDrawable(backgroundDrawable) holder.avatarImageView.setImageDrawable(backgroundDrawable)
} else { } else {
holder.avatarCheckedImageView.visibility = View.GONE holder.avatarCheckedImageView.visibility = View.GONE
avatarRenderer.render(avatarUrl, userId, name, holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
} }
} }

View File

@ -30,7 +30,7 @@ import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams import im.vector.matrix.android.api.session.room.model.create.CreateRoomParams
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.riotx.core.extensions.postLiveEvent import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.platform.VectorViewModel import im.vector.riotx.core.platform.VectorViewModel
@ -142,7 +142,7 @@ class CreateDirectRoomViewModel @AssistedInject constructor(@Assisted
session.rx() session.rx()
.searchUsersDirectory(search, 50, emptySet()) .searchUsersDirectory(search, 50, emptySet())
.map { users -> .map { users ->
users.sortedBy { it.displayName.firstLetterOfDisplayName() } users.sortedBy { it.toMatrixItem().firstLetterOfDisplayName() }
} }
} }
stream.toAsync { stream.toAsync {

View File

@ -19,9 +19,13 @@
package im.vector.riotx.features.home.createdirect package im.vector.riotx.features.home.createdirect
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import com.airbnb.mvrx.* import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.errorWithRetryItem import im.vector.riotx.core.epoxy.errorWithRetryItem
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
@ -94,9 +98,7 @@ class DirectoryUsersController @Inject constructor(private val session: Session,
createDirectRoomUserItem { createDirectRoomUserItem {
id(user.userId) id(user.userId)
selected(isSelected) selected(isSelected)
userId(user.userId) matrixItem(user.toMatrixItem())
name(user.displayName)
avatarUrl(user.avatarUrl)
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
clickListener { _ -> clickListener { _ ->
callback?.onItemClick(user) callback?.onItemClick(user)

View File

@ -23,7 +23,7 @@ import com.airbnb.mvrx.Incomplete
import com.airbnb.mvrx.Uninitialized import com.airbnb.mvrx.Uninitialized
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.EmptyItem_ import im.vector.riotx.core.epoxy.EmptyItem_
import im.vector.riotx.core.epoxy.loadingItem import im.vector.riotx.core.epoxy.loadingItem
@ -68,9 +68,7 @@ class KnownUsersController @Inject constructor(private val session: Session,
CreateDirectRoomUserItem_() CreateDirectRoomUserItem_()
.id(item.userId) .id(item.userId)
.selected(isSelected) .selected(isSelected)
.userId(item.userId) .matrixItem(item.toMatrixItem())
.name(item.displayName)
.avatarUrl(item.avatarUrl)
.avatarRenderer(avatarRenderer) .avatarRenderer(avatarRenderer)
.clickListener { _ -> .clickListener { _ ->
callback?.onItemClick(item) callback?.onItemClick(item)
@ -87,8 +85,8 @@ class KnownUsersController @Inject constructor(private val session: Session,
var lastFirstLetter: String? = null var lastFirstLetter: String? = null
for (model in models) { for (model in models) {
if (model is CreateDirectRoomUserItem) { if (model is CreateDirectRoomUserItem) {
if (model.userId == session.myUserId) continue if (model.matrixItem.id == session.myUserId) continue
val currentFirstLetter = model.name.firstLetterOfDisplayName() val currentFirstLetter = model.matrixItem.firstLetterOfDisplayName()
val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter val showLetter = !isFiltering && currentFirstLetter.isNotEmpty() && lastFirstLetter != currentFirstLetter
lastFirstLetter = currentFirstLetter lastFirstLetter = currentFirstLetter

View File

@ -36,7 +36,7 @@ import im.vector.riotx.core.utils.LiveEvent
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.functions.BiFunction import io.reactivex.functions.BiFunction
const val ALL_COMMUNITIES_GROUP_ID = "ALL_COMMUNITIES_GROUP_ID" const val ALL_COMMUNITIES_GROUP_ID = "+ALL_COMMUNITIES_GROUP_ID"
class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState, class GroupListViewModel @AssistedInject constructor(@Assisted initialState: GroupListViewState,
private val selectedGroupStore: SelectedGroupDataSource, private val selectedGroupStore: SelectedGroupDataSource,

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.group
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.session.group.model.GroupSummary import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
@ -49,10 +50,8 @@ class GroupSummaryController @Inject constructor(private val avatarRenderer: Ava
groupSummaryItem { groupSummaryItem {
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
id(groupSummary.groupId) id(groupSummary.groupId)
groupId(groupSummary.groupId) matrixItem(groupSummary.toMatrixItem())
groupName(groupSummary.displayName)
selected(isSelected) selected(isSelected)
avatarUrl(groupSummary.avatarUrl)
listener { callback?.onGroupSelected(groupSummary) } listener { callback?.onGroupSelected(groupSummary) }
} }
} }

View File

@ -20,6 +20,7 @@ import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -30,18 +31,16 @@ import im.vector.riotx.features.home.AvatarRenderer
abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() { abstract class GroupSummaryItem : VectorEpoxyModel<GroupSummaryItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var groupName: CharSequence @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute lateinit var groupId: String
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var selected: Boolean = false @EpoxyAttribute var selected: Boolean = false
@EpoxyAttribute var listener: (() -> Unit)? = null @EpoxyAttribute var listener: (() -> Unit)? = null
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
super.bind(holder) super.bind(holder)
holder.rootView.setOnClickListener { listener?.invoke() } holder.rootView.setOnClickListener { listener?.invoke() }
holder.groupNameView.text = groupName holder.groupNameView.text = matrixItem.displayName
holder.rootView.isChecked = selected holder.rootView.isChecked = selected
avatarRenderer.render(avatarUrl, groupId, groupName.toString(), holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
} }
class Holder : VectorEpoxyHolder() { class Holder : VectorEpoxyHolder() {

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.home.room.breadcrumbs
import android.view.View import android.view.View
import com.airbnb.epoxy.EpoxyController import com.airbnb.epoxy.EpoxyController
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.core.utils.DebouncedClickListener import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import javax.inject.Inject import javax.inject.Inject
@ -52,9 +53,7 @@ class BreadcrumbsController @Inject constructor(
breadcrumbsItem { breadcrumbsItem {
id(it.roomId) id(it.roomId)
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
roomId(it.roomId) matrixItem(it.toMatrixItem())
roomName(it.displayName)
avatarUrl(it.avatarUrl)
unreadNotificationCount(it.notificationCount) unreadNotificationCount(it.notificationCount)
showHighlighted(it.highlightCount > 0) showHighlighted(it.highlightCount > 0)
hasUnreadMessage(it.hasUnreadMessages) hasUnreadMessage(it.hasUnreadMessages)

View File

@ -48,6 +48,7 @@ class BreadcrumbsFragment @Inject constructor(
override fun onDestroyView() { override fun onDestroyView() {
breadcrumbsRecyclerView.cleanup() breadcrumbsRecyclerView.cleanup()
breadcrumbsController.listener = null
super.onDestroyView() super.onDestroyView()
} }
@ -56,6 +57,7 @@ class BreadcrumbsFragment @Inject constructor(
breadcrumbsController.listener = this breadcrumbsController.listener = this
} }
// TODO Use invalidate() ?
private fun renderState(state: BreadcrumbsViewState) { private fun renderState(state: BreadcrumbsViewState) {
breadcrumbsController.update(state) breadcrumbsController.update(state)
} }
@ -65,4 +67,8 @@ class BreadcrumbsFragment @Inject constructor(
override fun onBreadcrumbClicked(roomId: String) { override fun onBreadcrumbClicked(roomId: String) {
sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId)) sharedActionViewModel.post(RoomDetailSharedAction.SwitchToRoom(roomId))
} }
fun scrollToTop() {
breadcrumbsRecyclerView.scrollToPosition(0)
}
} }

View File

@ -22,6 +22,7 @@ import android.widget.ImageView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.core.epoxy.VectorEpoxyModel import im.vector.riotx.core.epoxy.VectorEpoxyModel
@ -32,9 +33,7 @@ import im.vector.riotx.features.home.room.list.UnreadCounterBadgeView
abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() { abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
@EpoxyAttribute lateinit var roomId: String @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute lateinit var roomName: CharSequence
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var unreadNotificationCount: Int = 0 @EpoxyAttribute var unreadNotificationCount: Int = 0
@EpoxyAttribute var showHighlighted: Boolean = false @EpoxyAttribute var showHighlighted: Boolean = false
@EpoxyAttribute var hasUnreadMessage: Boolean = false @EpoxyAttribute var hasUnreadMessage: Boolean = false
@ -45,7 +44,7 @@ abstract class BreadcrumbsItem : VectorEpoxyModel<BreadcrumbsItem.Holder>() {
super.bind(holder) super.bind(holder)
holder.rootView.setOnClickListener(itemClickListener) holder.rootView.setOnClickListener(itemClickListener)
holder.unreadIndentIndicator.isVisible = hasUnreadMessage holder.unreadIndentIndicator.isVisible = hasUnreadMessage
avatarRenderer.render(avatarUrl, roomId, roomName.toString(), holder.avatarImageView) avatarRenderer.render(matrixItem, holder.avatarImageView)
holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted)) holder.unreadCounterBadgeView.render(UnreadCounterBadgeView.State(unreadNotificationCount, showHighlighted))
holder.draftIndentIndicator.isVisible = hasDraft holder.draftIndentIndicator.isVisible = hasDraft
} }

View File

@ -63,4 +63,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ClearSendQueue : RoomDetailAction() object ClearSendQueue : RoomDetailAction()
object ResendAll : RoomDetailAction() object ResendAll : RoomDetailAction()
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String, val otherdDeviceId: String) : RoomDetailAction()
data class DeclineVerificationRequest(val transactionId: String) : RoomDetailAction()
} }

View File

@ -86,9 +86,19 @@ class RoomDetailActivity : VectorBaseActivity(), ToolbarConfigurable {
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() { private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) { override fun onDrawerStateChanged(newState: Int) {
hideKeyboard() hideKeyboard()
if (!drawerLayout.isDrawerOpen(GravityCompat.START) && newState == DrawerLayout.STATE_DRAGGING) {
// User is starting to open the drawer, scroll the list to op
scrollBreadcrumbsToTop()
}
} }
} }
private fun scrollBreadcrumbsToTop() {
supportFragmentManager.fragments.filterIsInstance<BreadcrumbsFragment>()
.forEach { it.scrollToTop() }
}
override fun onBackPressed() { override fun onBackPressed() {
if (drawerLayout.isDrawerOpen(GravityCompat.START)) { if (drawerLayout.isDrawerOpen(GravityCompat.START)) {
drawerLayout.closeDrawer(GravityCompat.START) drawerLayout.closeDrawer(GravityCompat.START)

View File

@ -66,10 +66,11 @@ import im.vector.matrix.android.api.session.room.timeline.Timeline
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.user.model.User import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.matrix.android.api.util.toMatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.dialogs.withColoredButton import im.vector.riotx.core.dialogs.withColoredButton
import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer import im.vector.riotx.core.epoxy.LayoutManagerStateRestorer
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.extensions.* import im.vector.riotx.core.extensions.*
import im.vector.riotx.core.files.addEntryToDownloadManager import im.vector.riotx.core.files.addEntryToDownloadManager
import im.vector.riotx.core.glide.GlideApp import im.vector.riotx.core.glide.GlideApp
@ -141,7 +142,6 @@ class RoomDetailFragment @Inject constructor(
private val notificationDrawerManager: NotificationDrawerManager, private val notificationDrawerManager: NotificationDrawerManager,
val roomDetailViewModelFactory: RoomDetailViewModel.Factory, val roomDetailViewModelFactory: RoomDetailViewModel.Factory,
val textComposerViewModelFactory: TextComposerViewModel.Factory, val textComposerViewModelFactory: TextComposerViewModel.Factory,
private val errorFormatter: ErrorFormatter,
private val eventHtmlRenderer: EventHtmlRenderer, private val eventHtmlRenderer: EventHtmlRenderer,
private val vectorPreferences: VectorPreferences private val vectorPreferences: VectorPreferences
) : ) :
@ -410,9 +410,7 @@ class RoomDetailFragment @Inject constructor(
composerLayout.sendButton.setContentDescription(getString(descriptionRes)) composerLayout.sendButton.setContentDescription(getString(descriptionRes))
avatarRenderer.render( avatarRenderer.render(
event.senderAvatar, MatrixItem.UserItem(event.root.senderId ?: "", event.getDisambiguatedDisplayName(), event.senderAvatar),
event.root.senderId ?: "",
event.getDisambiguatedDisplayName(),
composerLayout.composerRelatedMessageAvatar composerLayout.composerRelatedMessageAvatar
) )
composerLayout.expand { composerLayout.expand {
@ -601,20 +599,19 @@ class RoomDetailFragment @Inject constructor(
} }
// Replace the word by its completion // Replace the word by its completion
val displayName = item.displayName ?: item.userId val matrixItem = item.toMatrixItem()
val displayName = matrixItem.getBestName()
// with a trailing space // with a trailing space
editable.replace(startIndex, endIndex, "$displayName ") editable.replace(startIndex, endIndex, "$displayName ")
// Add the span // Add the span
val user = session.getUser(item.userId)
val span = PillImageSpan( val span = PillImageSpan(
glideRequests, glideRequests,
avatarRenderer, avatarRenderer,
requireContext(), requireContext(),
item.userId, matrixItem
user?.displayName ?: item.userId, )
user?.avatarUrl)
span.bind(composerLayout.composerEditText) span.bind(composerLayout.composerEditText)
editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) editable.setSpan(span, startIndex, startIndex + displayName.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
@ -686,7 +683,7 @@ class RoomDetailFragment @Inject constructor(
inviteView.visibility = View.GONE inviteView.visibility = View.GONE
val uid = session.myUserId val uid = session.myUserId
val meMember = session.getRoom(state.roomId)?.getRoomMember(uid) val meMember = session.getRoom(state.roomId)?.getRoomMember(uid)
avatarRenderer.render(meMember?.avatarUrl, uid, meMember?.displayName, composerLayout.composerAvatarImageView) avatarRenderer.render(MatrixItem.UserItem(uid, meMember?.displayName, meMember?.avatarUrl), composerLayout.composerAvatarImageView)
} else if (summary?.membership == Membership.INVITE && inviter != null) { } else if (summary?.membership == Membership.INVITE && inviter != null) {
inviteView.visibility = View.VISIBLE inviteView.visibility = View.VISIBLE
inviteView.render(inviter, VectorInviteView.Mode.LARGE) inviteView.render(inviter, VectorInviteView.Mode.LARGE)
@ -713,7 +710,7 @@ class RoomDetailFragment @Inject constructor(
activity?.finish() activity?.finish()
} else { } else {
roomToolbarTitleView.text = it.displayName roomToolbarTitleView.text = it.displayName
avatarRenderer.render(it, roomToolbarAvatarImageView) avatarRenderer.render(it.toMatrixItem(), roomToolbarAvatarImageView)
roomToolbarSubtitleView.setTextOrHide(it.topic) roomToolbarSubtitleView.setTextOrHide(it.topic)
} }
jumpToBottomView.count = it.notificationCount jumpToBottomView.count = it.notificationCount
@ -1024,6 +1021,10 @@ class RoomDetailFragment @Inject constructor(
.show(requireActivity().supportFragmentManager, "DISPLAY_EDITS") .show(requireActivity().supportFragmentManager, "DISPLAY_EDITS")
} }
override fun onTimelineItemAction(itemAction: RoomDetailAction) {
roomDetailViewModel.handle(itemAction)
}
override fun onRoomCreateLinkClicked(url: String) { override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor {
override fun navToRoom(roomId: String, eventId: String?): Boolean { override fun navToRoom(roomId: String, eventId: String?): Boolean {
@ -1197,9 +1198,8 @@ class RoomDetailFragment @Inject constructor(
glideRequests, glideRequests,
avatarRenderer, avatarRenderer,
requireContext(), requireContext(),
userId, MatrixItem.UserItem(userId, displayName, roomMember?.avatarUrl)
displayName, )
roomMember?.avatarUrl)
.also { it.bind(composerLayout.composerEditText) }, .also { it.bind(composerLayout.composerEditText) },
0, 0,
displayName.length, displayName.length,

View File

@ -48,6 +48,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineSettings
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.android.internal.crypto.model.rest.KeyVerificationStart
import im.vector.matrix.rx.rx import im.vector.matrix.rx.rx
import im.vector.matrix.rx.unwrap import im.vector.matrix.rx.unwrap
import im.vector.riotx.R import im.vector.riotx.R
@ -177,6 +178,8 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action) is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages() is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages() is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
} }
} }
@ -786,6 +789,21 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
}) })
} }
private fun handleAcceptVerification(action: RoomDetailAction.AcceptVerificationRequest) {
session.getSasVerificationService().beginKeyVerificationInDMs(
KeyVerificationStart.VERIF_METHOD_SAS,
action.transactionId,
room.roomId,
action.otherUserId,
action.otherdDeviceId,
null
)
}
private fun handleDeclineVerification(action: RoomDetailAction.DeclineVerificationRequest) {
Timber.e("TODO implement $action")
}
private fun observeSyncState() { private fun observeSyncState() {
session.rx() session.rx()
.liveSyncState() .liveSyncState()

View File

@ -58,7 +58,7 @@ data class RoomDetailViewState(
val isEncrypted: Boolean = false, val isEncrypted: Boolean = false,
val tombstoneEvent: Event? = null, val tombstoneEvent: Event? = null,
val tombstoneEventHandling: Async<String> = Uninitialized, val tombstoneEventHandling: Async<String> = Uninitialized,
val syncState: SyncState = SyncState.IDLE, val syncState: SyncState = SyncState.Idle,
val highlightedEventId: String? = null, val highlightedEventId: String? = null,
val unreadState: UnreadState = UnreadState.Unknown, val unreadState: UnreadState = UnreadState.Unknown,
val canShowJumpToReadMarker: Boolean = true val canShowJumpToReadMarker: Boolean = true

View File

@ -22,6 +22,7 @@ import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import com.airbnb.epoxy.EpoxyModelWithHolder import com.airbnb.epoxy.EpoxyModelWithHolder
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.R import im.vector.riotx.R
import im.vector.riotx.core.epoxy.VectorEpoxyHolder import im.vector.riotx.core.epoxy.VectorEpoxyHolder
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
@ -29,15 +30,13 @@ import im.vector.riotx.features.home.AvatarRenderer
@EpoxyModelClass(layout = R.layout.item_display_read_receipt) @EpoxyModelClass(layout = R.layout.item_display_read_receipt)
abstract class DisplayReadReceiptItem : EpoxyModelWithHolder<DisplayReadReceiptItem.Holder>() { abstract class DisplayReadReceiptItem : EpoxyModelWithHolder<DisplayReadReceiptItem.Holder>() {
@EpoxyAttribute var name: String? = null @EpoxyAttribute lateinit var matrixItem: MatrixItem
@EpoxyAttribute var userId: String = ""
@EpoxyAttribute var avatarUrl: String? = null
@EpoxyAttribute var timestamp: CharSequence? = null @EpoxyAttribute var timestamp: CharSequence? = null
@EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer @EpoxyAttribute lateinit var avatarRenderer: AvatarRenderer
override fun bind(holder: Holder) { override fun bind(holder: Holder) {
avatarRenderer.render(avatarUrl, userId, name, holder.avatarView) avatarRenderer.render(matrixItem, holder.avatarView)
holder.displayNameView.text = name ?: userId holder.displayNameView.text = matrixItem.getBestName()
timestamp?.let { timestamp?.let {
holder.timestampView.text = it holder.timestampView.text = it
holder.timestampView.isVisible = true holder.timestampView.isVisible = true

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.riotx.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.riotx.features.home.room.detail.timeline.item.toMatrixItem
import javax.inject.Inject import javax.inject.Inject
/** /**
@ -36,9 +37,7 @@ class DisplayReadReceiptsController @Inject constructor(private val dateFormatte
val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp) val timestamp = dateFormatter.formatRelativeDateTime(it.timestamp)
DisplayReadReceiptItem_() DisplayReadReceiptItem_()
.id(it.userId) .id(it.userId)
.userId(it.userId) .matrixItem(it.toMatrixItem())
.avatarUrl(it.avatarUrl)
.name(it.displayName)
.avatarRenderer(avatarRender) .avatarRenderer(avatarRender)
.timestamp(timestamp) .timestamp(timestamp)
.addIf(session.myUserId != it.userId, this) .addIf(session.myUserId != it.userId, this)

View File

@ -31,6 +31,7 @@ import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.riotx.core.date.VectorDateFormatter import im.vector.riotx.core.date.VectorDateFormatter
import im.vector.riotx.core.epoxy.LoadingItem_ import im.vector.riotx.core.epoxy.LoadingItem_
import im.vector.riotx.core.extensions.localDateTime import im.vector.riotx.core.extensions.localDateTime
import im.vector.riotx.features.home.room.detail.RoomDetailAction
import im.vector.riotx.features.home.room.detail.RoomDetailViewState import im.vector.riotx.features.home.room.detail.RoomDetailViewState
import im.vector.riotx.features.home.room.detail.UnreadState import im.vector.riotx.features.home.room.detail.UnreadState
import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.riotx.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
@ -62,6 +63,9 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent) fun onFileMessageClicked(eventId: String, messageFileContent: MessageFileContent)
fun onAudioMessageClicked(messageAudioContent: MessageAudioContent) fun onAudioMessageClicked(messageAudioContent: MessageAudioContent)
fun onEditedDecorationClicked(informationData: MessageInformationData) fun onEditedDecorationClicked(informationData: MessageInformationData)
// TODO move all callbacks to this?
fun onTimelineItemAction(itemAction: RoomDetailAction)
} }
interface ReactionPillCallback { interface ReactionPillCallback {

View File

@ -44,9 +44,7 @@ class MessageActionsEpoxyController @Inject constructor(private val stringProvid
bottomSheetMessagePreviewItem { bottomSheetMessagePreviewItem {
id("preview") id("preview")
avatarRenderer(avatarRenderer) avatarRenderer(avatarRenderer)
avatarUrl(state.informationData.avatarUrl ?: "") matrixItem(state.informationData.matrixItem)
senderId(state.informationData.senderId)
senderName(state.senderName())
movementMethod(createLinkMovementMethod(listener)) movementMethod(createLinkMovementMethod(listener))
body(body.linkify(listener)) body(body.linkify(listener))
time(state.time()) time(state.time())

View File

@ -23,10 +23,7 @@ import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.isTextMessage import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageContent import im.vector.matrix.android.api.session.room.model.message.*
import im.vector.matrix.android.api.session.room.model.message.MessageImageContent
import im.vector.matrix.android.api.session.room.model.message.MessageTextContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.send.SendState import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
@ -172,6 +169,8 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) { if (messageContent is MessageTextContent && messageContent.format == MessageType.FORMAT_MATRIX_HTML) {
eventHtmlRenderer.get().render(messageContent.formattedBody eventHtmlRenderer.get().render(messageContent.formattedBody
?: messageContent.body) ?: messageContent.body)
} else if (messageContent is MessageVerificationRequestContent) {
stringProvider.getString(R.string.verification_request)
} else { } else {
messageContent?.body messageContent?.body
} }

View File

@ -17,6 +17,7 @@
package im.vector.riotx.features.home.room.detail.timeline.factory package im.vector.riotx.features.home.room.detail.timeline.factory
import android.view.View import android.view.View
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import im.vector.matrix.android.api.session.events.model.EventType import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
@ -34,7 +35,8 @@ import javax.inject.Inject
class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider, class EncryptionItemFactory @Inject constructor(private val stringProvider: StringProvider,
private val avatarRenderer: AvatarRenderer, private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider) { private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
highlight: Boolean, highlight: Boolean,
@ -46,7 +48,8 @@ class EncryptionItemFactory @Inject constructor(private val stringProvider: Stri
sendState = event.root.sendState, sendState = event.root.sendState,
avatarUrl = event.senderAvatar, avatarUrl = event.senderAvatar,
memberName = event.getDisambiguatedDisplayName(), memberName = event.getDisambiguatedDisplayName(),
showInformation = false showInformation = false,
sentByMe = event.root.senderId == session.myUserId
) )
val attributes = NoticeItem.Attributes( val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer, avatarRenderer = avatarRenderer,

View File

@ -24,6 +24,7 @@ import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.View import android.view.View
import dagger.Lazy import dagger.Lazy
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.events.model.RelationType import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.* import im.vector.matrix.android.api.session.room.model.message.*
@ -64,7 +65,8 @@ class MessageItemFactory @Inject constructor(
private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder, private val contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider) { private val avatarSizeProvider: AvatarSizeProvider,
private val session: Session) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -97,14 +99,15 @@ class MessageItemFactory @Inject constructor(
// val all = event.root.toContent() // val all = event.root.toContent()
// val ev = all.toModel<Event>() // val ev = all.toModel<Event>()
return when (messageContent) { return when (messageContent) {
is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageEmoteContent -> buildEmoteMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes) is MessageTextContent -> buildItemForTextContent(messageContent, informationData, highlight, callback, attributes)
is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageImageInfoContent -> buildImageMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageNoticeContent -> buildNoticeMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageVideoContent -> buildVideoMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageFileContent -> buildFileMessageItem(messageContent, informationData, highlight, callback, attributes)
is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes) is MessageAudioContent -> buildAudioMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback) is MessageVerificationRequestContent -> buildVerificationRequestMessageItem(messageContent, informationData, highlight, callback, attributes)
else -> buildNotHandledMessageItem(messageContent, informationData, highlight, callback)
} }
} }
@ -128,6 +131,51 @@ class MessageItemFactory @Inject constructor(
})) }))
} }
private fun buildVerificationRequestMessageItem(messageContent: MessageVerificationRequestContent,
@Suppress("UNUSED_PARAMETER")
informationData: MessageInformationData,
highlight: Boolean,
callback: TimelineEventController.Callback?,
attributes: AbsMessageItem.Attributes): VerificationRequestItem? {
// If this request is not sent by me or sent to me, we should ignore it in timeline
val myUserId = session.myUserId
if (informationData.senderId != myUserId && messageContent.toUserId != myUserId) {
return null
}
val otherUserId = if (informationData.sentByMe) messageContent.toUserId else informationData.senderId
val otherUserName = if (informationData.sentByMe) session.getUser(messageContent.toUserId)?.displayName
else informationData.memberName
return VerificationRequestItem_()
.attributes(
VerificationRequestItem.Attributes(
otherUserId,
otherUserName.toString(),
messageContent.fromDevice,
informationData.eventId,
informationData,
attributes.avatarRenderer,
attributes.colorProvider,
attributes.itemLongClickListener,
attributes.itemClickListener,
attributes.reactionPillCallback,
attributes.readReceiptsCallback,
attributes.emojiTypeFace
)
)
.callback(callback)
// .izLocalFile(messageContent.getFileUrl().isLocalFile())
// .contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
// .filename(messageContent.body)
// .iconRes(R.drawable.filetype_audio)
// .clickListener(
// DebouncedClickListener(View.OnClickListener {
// callback?.onAudioMessageClicked(messageContent)
// }))
}
private fun buildFileMessageItem(messageContent: MessageFileContent, private fun buildFileMessageItem(messageContent: MessageFileContent,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
@ -193,7 +241,8 @@ class MessageItemFactory @Inject constructor(
val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize() val (maxWidth, maxHeight) = timelineMediaSizeProvider.getMaxSize()
val thumbnailData = ImageContentRenderer.Data( val thumbnailData = ImageContentRenderer.Data(
filename = messageContent.body, filename = messageContent.body,
url = messageContent.videoInfo?.thumbnailFile?.url ?: messageContent.videoInfo?.thumbnailUrl, url = messageContent.videoInfo?.thumbnailFile?.url
?: messageContent.videoInfo?.thumbnailUrl,
elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(), elementToDecrypt = messageContent.videoInfo?.thumbnailFile?.toElementToDecrypt(),
height = messageContent.videoInfo?.height, height = messageContent.videoInfo?.height,
maxHeight = maxHeight, maxHeight = maxHeight,

View File

@ -28,7 +28,8 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
private val encryptedItemFactory: EncryptedItemFactory, private val encryptedItemFactory: EncryptedItemFactory,
private val noticeItemFactory: NoticeItemFactory, private val noticeItemFactory: NoticeItemFactory,
private val defaultItemFactory: DefaultItemFactory, private val defaultItemFactory: DefaultItemFactory,
private val roomCreateItemFactory: RoomCreateItemFactory) { private val roomCreateItemFactory: RoomCreateItemFactory,
private val verificationConclusionItemFactory: VerificationItemFactory) {
fun create(event: TimelineEvent, fun create(event: TimelineEvent,
nextEvent: TimelineEvent?, nextEvent: TimelineEvent?,
@ -66,13 +67,15 @@ class TimelineItemFactory @Inject constructor(private val messageItemFactory: Me
} }
EventType.KEY_VERIFICATION_ACCEPT, EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_START, EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_KEY, EventType.KEY_VERIFICATION_KEY,
EventType.KEY_VERIFICATION_MAC -> { EventType.KEY_VERIFICATION_MAC -> {
// These events are filtered from timeline in normal case // These events are filtered from timeline in normal case
// Only visible in developer mode // Only visible in developer mode
defaultItemFactory.create(event, highlight, callback) noticeItemFactory.create(event, highlight, callback)
}
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_DONE -> {
verificationConclusionItemFactory.create(event, highlight, callback)
} }
// Unhandled event types (yet) // Unhandled event types (yet)

View File

@ -0,0 +1,154 @@
/*
* Copyright 2019 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.riotx.features.home.room.detail.timeline.factory
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.crypto.sas.CancelCode
import im.vector.matrix.android.api.session.crypto.sas.safeValueOf
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.events.model.RelationType
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.room.model.message.MessageRelationContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationCancelContent
import im.vector.matrix.android.api.session.room.model.message.MessageVerificationRequestContent
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.internal.session.room.VerificationState
import im.vector.riotx.core.epoxy.VectorEpoxyModel
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.UserPreferencesProvider
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.riotx.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem
import im.vector.riotx.features.home.room.detail.timeline.item.VerificationRequestConclusionItem_
import javax.inject.Inject
/**
* Can creates verification conclusion items
* Notice that not all KEY_VERIFICATION_DONE will be displayed in timeline,
* several checks are made to see if this conclusion is attached to a known request
*/
class VerificationItemFactory @Inject constructor(
private val colorProvider: ColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider,
private val noticeItemFactory: NoticeItemFactory,
private val userPreferencesProvider: UserPreferencesProvider,
private val session: Session
) {
fun create(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (event.root.eventId == null) return null
val relContent: MessageRelationContent = event.root.content.toModel()
?: event.root.getClearContent().toModel()
?: return ignoredConclusion(event, highlight, callback)
if (relContent.relatesTo?.type != RelationType.REFERENCE) return ignoredConclusion(event, highlight, callback)
val refEventId = relContent.relatesTo?.eventId
?: return ignoredConclusion(event, highlight, callback)
// If we cannot find the referenced request we do not display the done event
val refEvent = session.getRoom(event.root.roomId ?: "")?.getTimeLineEvent(refEventId)
?: return ignoredConclusion(event, highlight, callback)
// If it's not a request ignore this event
if (refEvent.root.getClearContent().toModel<MessageVerificationRequestContent>() == null) return ignoredConclusion(event, highlight, callback)
val referenceInformationData = messageInformationDataFactory.create(refEvent, null)
val informationData = messageInformationDataFactory.create(event, null)
val attributes = messageItemAttributesFactory.create(null, informationData, callback)
when (event.root.getClearType()) {
EventType.KEY_VERIFICATION_CANCEL -> {
// Is the request referenced is actually really cancelled?
val cancelContent = event.root.getClearContent().toModel<MessageVerificationCancelContent>()
?: return ignoredConclusion(event, highlight, callback)
when (safeValueOf(cancelContent.code)) {
CancelCode.MismatchedCommitment,
CancelCode.MismatchedKeys,
CancelCode.MismatchedSas -> {
// We should display these bad conclusions
return VerificationRequestConclusionItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = false,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
colorProvider = colorProvider,
emojiTypeFace = attributes.emojiTypeFace,
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
else -> ignoredConclusion(event, highlight, callback)
}
}
EventType.KEY_VERIFICATION_DONE -> {
// Is the request referenced is actually really completed?
if (referenceInformationData.referencesInfoData?.verificationStatus != VerificationState.DONE)
return ignoredConclusion(event, highlight, callback)
// We only tale the one sent by me
if (informationData.sentByMe) {
// We only display the done sent by the other user, the done send by me is ignored
return ignoredConclusion(event, highlight, callback)
}
return VerificationRequestConclusionItem_()
.attributes(
VerificationRequestConclusionItem.Attributes(
toUserId = informationData.senderId,
toUserName = informationData.memberName.toString(),
isPositive = true,
informationData = informationData,
avatarRenderer = attributes.avatarRenderer,
colorProvider = colorProvider,
emojiTypeFace = attributes.emojiTypeFace,
itemClickListener = attributes.itemClickListener,
itemLongClickListener = attributes.itemLongClickListener,
reactionPillCallback = attributes.reactionPillCallback,
readReceiptsCallback = attributes.readReceiptsCallback
)
)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}
return null
}
private fun ignoredConclusion(event: TimelineEvent,
highlight: Boolean,
callback: TimelineEventController.Callback?
): VectorEpoxyModel<*>? {
if (userPreferencesProvider.shouldShowHiddenEvents()) return noticeItemFactory.create(event, highlight, callback)
return null
}
}

View File

@ -0,0 +1,104 @@
/*
* Copyright 2019 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.riotx.features.home.room.detail.timeline.format
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.api.session.room.model.message.MessageType
import im.vector.matrix.android.api.session.room.model.message.isReply
import im.vector.matrix.android.api.session.room.timeline.TimelineEvent
import im.vector.matrix.android.api.session.room.timeline.getLastMessageContent
import im.vector.matrix.android.api.session.room.timeline.getTextEditableContent
import im.vector.riotx.R
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import me.gujun.android.span.span
import javax.inject.Inject
class DisplayableEventFormatter @Inject constructor(
// private val sessionHolder: ActiveSessionHolder,
private val stringProvider: StringProvider,
private val colorProvider: ColorProvider,
private val noticeEventFormatter: NoticeEventFormatter
) {
fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
if (timelineEvent.root.isEncrypted()
&& timelineEvent.root.mxDecryptionResult == null) {
return stringProvider.getString(R.string.encrypted_message)
}
val senderName = timelineEvent.getDisambiguatedDisplayName()
when (timelineEvent.root.getClearType()) {
EventType.MESSAGE -> {
timelineEvent.getLastMessageContent()?.let { messageContent ->
when (messageContent.type) {
MessageType.MSGTYPE_VERIFICATION_REQUEST -> {
return simpleFormat(senderName, stringProvider.getString(R.string.verification_request), appendAuthor)
}
MessageType.MSGTYPE_IMAGE -> {
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_image), appendAuthor)
}
MessageType.MSGTYPE_AUDIO -> {
return simpleFormat(senderName, stringProvider.getString(R.string.sent_an_audio_file), appendAuthor)
}
MessageType.MSGTYPE_VIDEO -> {
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_video), appendAuthor)
}
MessageType.MSGTYPE_FILE -> {
return simpleFormat(senderName, stringProvider.getString(R.string.sent_a_file), appendAuthor)
}
MessageType.MSGTYPE_TEXT -> {
if (messageContent.isReply()) {
// Skip reply prefix, and show important
// TODO add a reply image span ?
return simpleFormat(senderName, timelineEvent.getTextEditableContent()
?: messageContent.body, appendAuthor)
} else {
return simpleFormat(senderName, messageContent.body, appendAuthor)
}
}
else -> {
return simpleFormat(senderName, messageContent.body, appendAuthor)
}
}
}
}
else -> {
return span {
text = noticeEventFormatter.format(timelineEvent) ?: ""
textStyle = "italic"
}
}
}
return span { }
}
private fun simpleFormat(senderName: String, body: CharSequence, appendAuthor: Boolean): CharSequence {
return if (appendAuthor) {
span {
text = senderName
textColor = colorProvider.getColorFromAttribute(R.attr.riotx_text_primary)
}
.append(": ")
.append(body)
} else {
body
}
}
}

View File

@ -44,6 +44,12 @@ class NoticeEventFormatter @Inject constructor(private val sessionHolder: Active
EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName()) EventType.CALL_ANSWER -> formatCallEvent(timelineEvent.root, timelineEvent.getDisambiguatedDisplayName())
EventType.MESSAGE, EventType.MESSAGE,
EventType.REACTION, EventType.REACTION,
EventType.KEY_VERIFICATION_START,
EventType.KEY_VERIFICATION_CANCEL,
EventType.KEY_VERIFICATION_ACCEPT,
EventType.KEY_VERIFICATION_MAC,
EventType.KEY_VERIFICATION_DONE,
EventType.KEY_VERIFICATION_KEY,
EventType.REDACTION -> formatDebug(timelineEvent.root) EventType.REDACTION -> formatDebug(timelineEvent.root)
else -> { else -> {
Timber.v("Type $type not handled by this formatter") Timber.v("Type $type not handled by this formatter")

Some files were not shown because too many files have changed in this diff Show More