Merge branch 'develop' into feature/initial_sync

This commit is contained in:
ganfra 2019-12-16 19:11:04 +01:00
commit 2316c98a65
180 changed files with 5671 additions and 898 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

@ -1,4 +1,28 @@
Changes in RiotX 0.10.0 (2019-XX-XX) Changes in RiotX 0.11.0 (2019-XX-XX)
===================================================
Features ✨:
- Implement soft logout (#281)
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
Other changes:
- Use same default room colors than Riot-Web
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 🗣:
-
Build 🧱:
- Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10)
=================================================== ===================================================
Features ✨: Features ✨:
@ -15,12 +39,6 @@ Bugfix 🐛:
- "ban" event are not rendered correctly (#716) - "ban" event are not rendered correctly (#716)
- Fix crash when rotating screen in Room timeline - Fix crash when rotating screen in Room timeline
Translations 🗣:
-
Build 🧱:
-
Changes in RiotX 0.9.1 (2019-12-05) Changes in RiotX 0.9.1 (2019-12-05)
=================================================== ===================================================

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

@ -1,38 +0,0 @@
/*
* 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.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.CompletableEmitter
internal class MatrixCallbackCompletable<T>(private val completableEmitter: CompletableEmitter) : MatrixCallback<T> {
override fun onSuccess(data: T) {
completableEmitter.onComplete()
}
override fun onFailure(failure: Throwable) {
completableEmitter.tryOnError(failure)
}
}
fun Cancelable.toCompletable(completableEmitter: CompletableEmitter) {
completableEmitter.setCancellable {
this.cancel()
}
}

View file

@ -0,0 +1,54 @@
/*
* 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.rx
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.Completable
import io.reactivex.Single
fun <T> singleBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Single<T> = Single.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}
fun <T> completableBuilder(builder: (callback: MatrixCallback<T>) -> Cancelable): Completable = Completable.create {
val callback: MatrixCallback<T> = object : MatrixCallback<T> {
override fun onSuccess(data: T) {
it.onComplete()
}
override fun onFailure(failure: Throwable) {
it.tryOnError(failure)
}
}
val cancelable = builder(callback)
it.setCancellable {
cancelable.cancel()
}
}

View file

@ -53,13 +53,13 @@ class RxRoom(private val room: Room) {
return room.getMyReadReceiptLive().asObservable() return room.getMyReadReceiptLive().asObservable()
} }
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create { fun loadRoomMembersIfNeeded(): Single<Unit> = singleBuilder {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it) room.loadRoomMembersIfNeeded(it)
} }
fun joinRoom(reason: String? = null, fun joinRoom(reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create { viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) room.join(reason, viaServers, it)
} }
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> { fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {

View file

@ -66,20 +66,25 @@ class RxSession(private val session: Session) {
return session.livePagedUsers(filter).asObservable() return session.livePagedUsers(filter).asObservable()
} }
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create { fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it) session.createRoom(roomParams, it)
} }
fun searchUsersDirectory(search: String, fun searchUsersDirectory(search: String,
limit: Int, limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create { excludedUserIds: Set<String>): Single<List<User>> = singleBuilder {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it) session.searchUsersDirectory(search, limit, excludedUserIds, it)
} }
fun joinRoom(roomId: String, fun joinRoom(roomId: String,
reason: String? = null, reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create { viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it) session.joinRoom(roomId, reason, viaServers, it)
}
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean): Single<Optional<String>> = singleBuilder {
session.getRoomIdByAlias(roomAlias, searchOnServer, it)
} }
} }

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

@ -45,4 +45,4 @@ sealed class Failure(cause: Throwable? = null) : Throwable(cause = cause) {
abstract class FeatureFailure : Failure() abstract class FeatureFailure : Failure()
} }
fun Throwable.isTokenError() = this is Failure.ServerError && (this.error.code == MatrixError.UNKNOWN_TOKEN || this.error.code == MatrixError.MISSING_TOKEN) fun Throwable.isTokenError() = this is Failure.ServerError && (this.error.code == MatrixError.M_UNKNOWN_TOKEN || this.error.code == MatrixError.M_MISSING_TOKEN)

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

@ -24,9 +24,7 @@ import android.net.Uri
*/ */
sealed class PermalinkData { sealed class PermalinkData {
data class EventLink(val roomIdOrAlias: String, val eventId: String) : PermalinkData() data class RoomLink(val roomIdOrAlias: String, val isRoomAlias: Boolean, val eventId: String?) : PermalinkData()
data class RoomLink(val roomIdOrAlias: String) : PermalinkData()
data class UserLink(val userId: String) : PermalinkData() data class UserLink(val userId: String) : PermalinkData()

View file

@ -60,16 +60,21 @@ object PermalinkParser {
return PermalinkData.FallbackLink(uri) return PermalinkData.FallbackLink(uri)
} }
return when { return when {
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier) MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier) MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> { MatrixPatterns.isRoomId(identifier) -> {
if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) { val eventId = extraParameter.takeIf {
PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter) !it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
} else {
PermalinkData.RoomLink(roomIdOrAlias = identifier)
} }
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = false, eventId = eventId)
} }
else -> PermalinkData.FallbackLink(uri) MatrixPatterns.isRoomAlias(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
PermalinkData.RoomLink(roomIdOrAlias = identifier, isRoomAlias = true, eventId = eventId)
}
else -> PermalinkData.FallbackLink(uri)
} }
} }
} }

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)
@ -141,13 +146,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

@ -21,6 +21,7 @@ import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
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.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
/** /**
* This interface defines methods to get rooms. It's implemented at the session level. * This interface defines methods to get rooms. It's implemented at the session level.
@ -74,4 +75,11 @@ interface RoomService {
*/ */
fun markAllAsRead(roomIds: List<String>, fun markAllAsRead(roomIds: List<String>,
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable
/**
* Resolve a room alias to a room ID.
*/
fun getRoomIdByAlias(roomAlias: String,
searchOnServer: Boolean,
callback: MatrixCallback<Optional<String>>): Cancelable
} }

View file

@ -59,7 +59,6 @@ interface MembershipService {
/** /**
* Join the room, or accept an invitation. * Join the room, or accept an invitation.
*/ */
fun join(reason: String? = null, fun join(reason: String? = null,
viaServers: List<String> = emptyList(), viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable callback: MatrixCallback<Unit>): Cancelable

View file

@ -29,6 +29,8 @@ data class RoomSummary(
val displayName: String = "", val displayName: String = "",
val topic: String = "", val topic: String = "",
val avatarUrl: String = "", val avatarUrl: String = "",
val canonicalAlias: String? = null,
val aliases: List<String> = emptyList(),
val isDirect: Boolean = false, val isDirect: Boolean = false,
val latestPreviewableEvent: TimelineEvent? = null, val latestPreviewableEvent: TimelineEvent? = null,
val otherMemberIds: List<String> = emptyList(), val otherMemberIds: List<String> = emptyList(),

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

@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor(
membership = roomSummaryEntity.membership, membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState, versioningState = roomSummaryEntity.versioningState,
readMarkerId = roomSummaryEntity.readMarkerId, readMarkerId = roomSummaryEntity.readMarkerId,
userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList() userDrafts = roomSummaryEntity.userDrafts?.userDrafts?.map { DraftMapper.map(it) } ?: emptyList(),
canonicalAlias = roomSummaryEntity.canonicalAlias,
aliases = roomSummaryEntity.aliases.toList()
) )
} }
} }

View file

@ -39,7 +39,10 @@ internal open class RoomSummaryEntity(@PrimaryKey var roomId: String = "",
var hasUnreadMessages: Boolean = false, var hasUnreadMessages: Boolean = false,
var tags: RealmList<RoomTagEntity> = RealmList(), var tags: RealmList<RoomTagEntity> = RealmList(),
var userDrafts: UserDraftsEntity? = null, var userDrafts: UserDraftsEntity? = null,
var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS var breadcrumbsIndex: Int = NOT_IN_BREADCRUMBS,
var canonicalAlias: String? = null,
var aliases: RealmList<String> = RealmList(),
var flatAliases: String = ""
) : RealmObject() { ) : RealmObject() {
private var membershipStr: String = Membership.NONE.name private var membershipStr: String = Membership.NONE.name

View file

@ -32,6 +32,18 @@ internal fun RoomSummaryEntity.Companion.where(realm: Realm, roomId: String? = n
return query return query
} }
internal fun RoomSummaryEntity.Companion.findByAlias(realm: Realm, roomAlias: String): RoomSummaryEntity? {
val roomSummary = realm.where<RoomSummaryEntity>()
.equalTo(RoomSummaryEntityFields.CANONICAL_ALIAS, roomAlias)
.findFirst()
if (roomSummary != null) {
return roomSummary
}
return realm.where<RoomSummaryEntity>()
.contains(RoomSummaryEntityFields.FLAT_ALIASES, "|$roomAlias")
.findFirst()
}
internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity { internal fun RoomSummaryEntity.Companion.getOrCreate(realm: Realm, roomId: String): RoomSummaryEntity {
return where(realm, roomId).findFirst() ?: realm.createObject(roomId) return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
} }

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

@ -20,8 +20,8 @@ package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException import com.squareup.moshi.JsonDataException
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
@ -32,6 +32,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
@ -99,7 +100,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,12 +42,16 @@ 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.SyncTaskSequencer import im.vector.matrix.android.internal.session.sync.SyncTaskSequencer
import im.vector.matrix.android.internal.session.sync.SyncTokenStore import im.vector.matrix.android.internal.session.sync.SyncTokenStore
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
@ -76,6 +80,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val contentUrlResolver: ContentUrlResolver, private val contentUrlResolver: ContentUrlResolver,
private val syncTokenStore: SyncTokenStore, private val syncTokenStore: SyncTokenStore,
private val syncTaskSequencer: SyncTaskSequencer, private val syncTaskSequencer: SyncTaskSequencer,
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>)
@ -98,6 +103,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()
@ -169,8 +177,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

@ -25,11 +25,13 @@ import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.room.model.VersioningState import im.vector.matrix.android.api.session.room.model.VersioningState
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.util.Cancelable import im.vector.matrix.android.api.util.Cancelable
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper import im.vector.matrix.android.internal.database.mapper.RoomSummaryMapper
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask import im.vector.matrix.android.internal.session.room.membership.joining.JoinRoomTask
import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask import im.vector.matrix.android.internal.session.room.read.MarkAllRoomsReadTask
@ -45,6 +47,7 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
private val joinRoomTask: JoinRoomTask, private val joinRoomTask: JoinRoomTask,
private val markAllRoomsReadTask: MarkAllRoomsReadTask, private val markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask, private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomFactory: RoomFactory, private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService { private val taskExecutor: TaskExecutor) : RoomService {
@ -111,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
} }
.executeBy(taskExecutor) .executeBy(taskExecutor)
} }
override fun getRoomIdByAlias(roomAlias: String, searchOnServer: Boolean, callback: MatrixCallback<Optional<String>>): Cancelable {
return roomIdByAliasTask
.configureWith(GetRoomIdByAliasTask.Params(roomAlias, searchOnServer)) {
this.callback = callback
}
.executeBy(taskExecutor)
}
} }

View file

@ -18,6 +18,7 @@ package im.vector.matrix.android.internal.session.room
import im.vector.matrix.android.api.session.events.model.Content import im.vector.matrix.android.api.session.events.model.Content
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.internal.session.room.alias.RoomAliasDescription
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.room.model.create.CreateRoomResponse import im.vector.matrix.android.api.session.room.model.create.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
@ -258,4 +259,12 @@ internal interface RoomAPI {
fun reportContent(@Path("roomId") roomId: String, fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String, @Path("eventId") eventId: String,
@Body body: ReportContentBody): Call<Unit> @Body body: ReportContentBody): Call<Unit>
/**
* Get the room ID associated to the room alias.
*
* @param roomAlias the room alias.
*/
@GET(NetworkConstants.URI_API_PREFIX_PATH_R0 + "directory/room/{roomAlias}")
fun getRoomIdByAlias(@Path("roomAlias") roomAlias: String): Call<RoomAliasDescription>
} }

View file

@ -24,6 +24,8 @@ import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.internal.session.DefaultFileService import im.vector.matrix.android.internal.session.DefaultFileService
import im.vector.matrix.android.internal.session.SessionScope import im.vector.matrix.android.internal.session.SessionScope
import im.vector.matrix.android.internal.session.room.alias.DefaultGetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.alias.GetRoomIdByAliasTask
import im.vector.matrix.android.internal.session.room.create.CreateRoomTask import im.vector.matrix.android.internal.session.room.create.CreateRoomTask
import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask import im.vector.matrix.android.internal.session.room.create.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask
@ -133,4 +135,7 @@ internal abstract class RoomModule {
@Binds @Binds
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
@Binds
abstract fun bindGetRoomIdByAliasTask(getRoomIdByAliasTask: DefaultGetRoomIdByAliasTask): GetRoomIdByAliasTask
} }

View file

@ -20,6 +20,8 @@ import com.zhuinden.monarchy.Monarchy
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
import im.vector.matrix.android.api.session.room.model.Membership import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.RoomAliasesContent
import im.vector.matrix.android.api.session.room.model.RoomCanonicalAliasContent
import im.vector.matrix.android.api.session.room.model.RoomTopicContent import im.vector.matrix.android.api.session.room.model.RoomTopicContent
import im.vector.matrix.android.internal.database.mapper.ContentMapper import im.vector.matrix.android.internal.database.mapper.ContentMapper
import im.vector.matrix.android.internal.database.model.EventEntity import im.vector.matrix.android.internal.database.model.EventEntity
@ -68,7 +70,7 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
unreadNotifications: RoomSyncUnreadNotifications? = null, unreadNotifications: RoomSyncUnreadNotifications? = null,
updateMembers: Boolean = false) { updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst() val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId) ?: realm.createObject(roomId)
if (roomSummary != null) { if (roomSummary != null) {
if (roomSummary.heroes.isNotEmpty()) { if (roomSummary.heroes.isNotEmpty()) {
@ -91,15 +93,24 @@ internal class RoomSummaryUpdater @Inject constructor(@UserId private val userId
val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES) val latestPreviewableEvent = TimelineEventEntity.latestEvent(realm, roomId, includesSending = true, filterTypes = PREVIEWABLE_TYPES)
val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev() val lastTopicEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_TOPIC).prev()
val lastCanonicalAliasEvent = EventEntity.where(realm, roomId, EventType.STATE_CANONICAL_ALIAS).prev()
val lastAliasesEvent = EventEntity.where(realm, roomId, EventType.STATE_ROOM_ALIASES).prev()
roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0 roomSummaryEntity.hasUnreadMessages = roomSummaryEntity.notificationCount > 0
// avoid this call if we are sure there are unread events // avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId) || !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString() roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId) roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent roomSummaryEntity.latestPreviewableEvent = latestPreviewableEvent
roomSummaryEntity.canonicalAlias = ContentMapper.map(lastCanonicalAliasEvent?.content).toModel<RoomCanonicalAliasContent>()
?.canonicalAlias
val roomAliases = ContentMapper.map(lastAliasesEvent?.content).toModel<RoomAliasesContent>()?.aliases ?: emptyList()
roomSummaryEntity.aliases.clear()
roomSummaryEntity.aliases.addAll(roomAliases)
roomSummaryEntity.flatAliases = roomAliases.joinToString(separator = "|", prefix = "|")
if (updateMembers) { if (updateMembers) {
val otherRoomMembers = RoomMembers(realm, roomId) val otherRoomMembers = RoomMembers(realm, roomId)

View file

@ -0,0 +1,54 @@
/*
* 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.room.alias
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.findByAlias
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.room.RoomAPI
import im.vector.matrix.android.internal.task.Task
import io.realm.Realm
import javax.inject.Inject
internal interface GetRoomIdByAliasTask : Task<GetRoomIdByAliasTask.Params, Optional<String>> {
data class Params(
val roomAlias: String,
val searchOnServer: Boolean
)
}
internal class DefaultGetRoomIdByAliasTask @Inject constructor(private val monarchy: Monarchy,
private val roomAPI: RoomAPI) : GetRoomIdByAliasTask {
override suspend fun execute(params: GetRoomIdByAliasTask.Params): Optional<String> {
var roomId = Realm.getInstance(monarchy.realmConfiguration).use {
RoomSummaryEntity.findByAlias(it, params.roomAlias)?.roomId
}
return if (roomId != null) {
Optional.from(roomId)
} else if (!params.searchOnServer) {
Optional.from<String>(null)
} else {
roomId = executeRequest<RoomAliasDescription> {
apiCall = roomAPI.getRoomIdByAlias(params.roomAlias)
}.roomId
Optional.from(roomId)
}
}
}

View file

@ -0,0 +1,33 @@
/*
* 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.room.alias
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
internal data class RoomAliasDescription(
/**
* The room ID for this alias.
*/
@Json(name = "room_id") val roomId: String,
/**
* A list of servers that are aware of this room ID.
*/
@Json(name = "servers") val servers: List<String> = emptyList()
)

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
@ -74,17 +72,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)
if (isInitialSync) { if (isInitialSync) {

View file

@ -19,6 +19,7 @@ import android.app.Service
import android.content.Intent import android.content.Intent
import android.os.IBinder import android.os.IBinder
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.isTokenError import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTask

View file

@ -21,6 +21,7 @@ import androidx.lifecycle.MutableLiveData
import com.squareup.moshi.JsonEncodingException import com.squareup.moshi.JsonEncodingException
import im.vector.matrix.android.api.failure.Failure import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.api.failure.isTokenError
import im.vector.matrix.android.api.session.sync.SyncState import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.matrix.android.internal.network.NetworkConnectivityChecker import im.vector.matrix.android.internal.network.NetworkConnectivityChecker
import im.vector.matrix.android.internal.session.sync.SyncTask import im.vector.matrix.android.internal.session.sync.SyncTask
@ -38,19 +39,20 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
private val backgroundDetectionObserver: BackgroundDetectionObserver) private val backgroundDetectionObserver: BackgroundDetectionObserver)
: 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 val syncScope = CoroutineScope(SupervisorJob()) private val syncScope = CoroutineScope(SupervisorJob())
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)
} }
@ -58,6 +60,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()
} }
} }
@ -72,7 +76,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)
syncScope.coroutineContext.cancelChildren() syncScope.coroutineContext.cancelChildren()
lock.notify() lock.notify()
} }
@ -93,24 +97,29 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
isStarted = true isStarted = true
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 params = SyncTask.Params(timeout) val params = SyncTask.Params(timeout)
val sync = syncScope.launch { val sync = syncScope.launch {
@ -123,7 +132,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)
} }
@ -137,11 +146,11 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
Timber.v("Timeout") Timber.v("Timeout")
} 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.isTokenError()) {
&& (failure.error.code == MatrixError.UNKNOWN_TOKEN || failure.error.code == MatrixError.MISSING_TOKEN)) {
// No token or invalid token, stop the thread // No token or invalid token, stop the thread
Timber.w(failure) Timber.w(failure)
updateStateTo(SyncState.KILLING) isStarted = false
isTokenValid = false
} else { } else {
Timber.e(failure) Timber.e(failure)
if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) { if (failure !is Failure.NetworkConnection || failure.cause is JsonEncodingException) {
@ -152,8 +161,8 @@ internal class SyncThread @Inject constructor(private val syncTask: SyncTask,
} }
} finally { } finally {
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))
} }
} }
} }

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

@ -15,7 +15,7 @@ androidExtensions {
} }
ext.versionMajor = 0 ext.versionMajor = 0
ext.versionMinor = 10 ext.versionMinor = 11
ext.versionPatch = 0 ext.versionPatch = 0
static def getGitTimestamp() { static def getGitTimestamp() {
@ -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

@ -65,7 +65,13 @@
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" /> <activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" /> <activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<activity android:name=".features.home.room.filtered.FilteredRoomsActivity" /> <activity android:name=".features.home.room.filtered.FilteredRoomsActivity" />
<activity android:name=".features.home.room.detail.RoomDetailActivity" /> <activity
android:name=".features.home.room.detail.RoomDetailActivity"
android:parentActivityName=".features.home.HomeActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".features.home.HomeActivity" />
</activity>
<activity android:name=".features.debug.DebugMenuActivity" /> <activity android:name=".features.debug.DebugMenuActivity" />
<activity android:name=".features.home.createdirect.CreateDirectRoomActivity" /> <activity android:name=".features.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" /> <activity android:name=".features.webview.VectorWebViewActivity" />
@ -98,6 +104,22 @@
<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" />
<activity android:name=".features.permalink.PermalinkHandlerActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="matrix.to" />
</intent-filter>
</activity>
<!-- 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 {
@ -261,4 +262,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
@ -40,6 +41,7 @@ import im.vector.riotx.features.login.LoginActivity
import im.vector.riotx.features.media.ImageMediaViewerActivity import im.vector.riotx.features.media.ImageMediaViewerActivity
import im.vector.riotx.features.media.VideoMediaViewerActivity import im.vector.riotx.features.media.VideoMediaViewerActivity
import im.vector.riotx.features.navigation.Navigator import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.permalink.PermalinkHandlerActivity
import im.vector.riotx.features.rageshake.BugReportActivity import im.vector.riotx.features.rageshake.BugReportActivity
import im.vector.riotx.features.rageshake.BugReporter import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.RageShake import im.vector.riotx.features.rageshake.RageShake
@ -49,6 +51,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 +81,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 +131,10 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet) fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
@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

@ -21,6 +21,7 @@ import androidx.core.content.ContextCompat
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.core.services.VectorSyncService import im.vector.riotx.core.services.VectorSyncService
import im.vector.riotx.features.notifications.PushRuleTriggerListener import im.vector.riotx.features.notifications.PushRuleTriggerListener
@ -59,3 +60,11 @@ fun Session.startSyncing(context: Context) {
Timber.v("--> is at least started? $isAtLeastStarted") Timber.v("--> is at least started? $isAtLeastStarted")
startSync(isAtLeastStarted) startSync(isAtLeastStarted)
} }
/**
* 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
@ -31,6 +33,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
@ -38,23 +44,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
@ -64,48 +84,75 @@ 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() {
val session = sessionHolder.getSafeActiveSession() val session = sessionHolder.getSafeActiveSession()
if (session == null) { if (session == null) {
start() startNextActivityAndFinish()
return return
} }
when { when {
clearCredentials -> session.signOut(object : MatrixCallback<Unit> { args.clearCredentials -> session.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 -> session.clearCache(object : MatrixCallback<Unit> { args.clearCache -> session.clearCache(
override fun onSuccess(data: Unit) { object : MatrixCallback<Unit> {
session.startSyncing(applicationContext) override fun onSuccess(data: Unit) {
doLocalCleanupAndStart() session.startSyncing(applicationContext)
} 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
@ -119,24 +166,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

@ -1,87 +0,0 @@
/*
* 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 android.content.Context
import android.net.Uri
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.navigation.Navigator
import javax.inject.Inject
class PermalinkHandler @Inject constructor(private val session: Session,
private val navigator: Navigator) {
fun launch(context: Context, deepLink: String?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
val uri = deepLink?.let { Uri.parse(it) }
return launch(context, uri, navigateToRoomInterceptor)
}
fun launch(context: Context, deepLink: Uri?, navigateToRoomInterceptor: NavigateToRoomInterceptor? = null): Boolean {
if (deepLink == null) {
return false
}
return when (val permalinkData = PermalinkParser.parse(deepLink)) {
is PermalinkData.EventLink -> {
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias, permalinkData.eventId) != true) {
openRoom(context, permalinkData.roomIdOrAlias, permalinkData.eventId)
}
true
}
is PermalinkData.RoomLink -> {
if (navigateToRoomInterceptor?.navToRoom(permalinkData.roomIdOrAlias) != true) {
openRoom(context, permalinkData.roomIdOrAlias)
}
true
}
is PermalinkData.GroupLink -> {
navigator.openGroupDetail(permalinkData.groupId, context)
true
}
is PermalinkData.UserLink -> {
navigator.openUserDetail(permalinkData.userId, context)
true
}
is PermalinkData.FallbackLink -> {
false
}
}
}
/**
* Open room either joined, or not unknown
*/
private fun openRoom(context: Context, roomIdOrAlias: String, eventId: String? = null) {
if (session.getRoom(roomIdOrAlias) != null) {
navigator.openRoom(context, roomIdOrAlias, eventId)
} else {
navigator.openNotJoinedRoom(context, roomIdOrAlias, eventId)
}
}
}
interface NavigateToRoomInterceptor {
/**
* Return true if the navigation has been intercepted
*/
fun navToRoom(roomId: String, eventId: String? = null): Boolean
}

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

@ -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
@ -85,8 +86,8 @@ import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
import im.vector.riotx.features.command.Command import im.vector.riotx.features.command.Command
import im.vector.riotx.features.home.AvatarRenderer import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.NavigateToRoomInterceptor import im.vector.riotx.features.permalink.NavigateToRoomInterceptor
import im.vector.riotx.features.home.PermalinkHandler import im.vector.riotx.features.permalink.PermalinkHandler
import im.vector.riotx.features.home.getColorFromUserId import im.vector.riotx.features.home.getColorFromUserId
import im.vector.riotx.features.home.room.detail.composer.TextComposerAction import im.vector.riotx.features.home.room.detail.composer.TextComposerAction
import im.vector.riotx.features.home.room.detail.composer.TextComposerView import im.vector.riotx.features.home.room.detail.composer.TextComposerView
@ -112,6 +113,8 @@ import im.vector.riotx.features.reactions.EmojiReactionPickerActivity
import im.vector.riotx.features.settings.VectorPreferences import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.share.SharedData import im.vector.riotx.features.share.SharedData
import im.vector.riotx.features.themes.ThemeUtils import im.vector.riotx.features.themes.ThemeUtils
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import kotlinx.android.parcel.Parcelize import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.merge_composer_layout.view.* import kotlinx.android.synthetic.main.merge_composer_layout.view.*
@ -141,7 +144,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 +412,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 +601,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 +685,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 +712,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
@ -854,30 +853,33 @@ class RoomDetailFragment @Inject constructor(
// TimelineEventController.Callback ************************************************************ // TimelineEventController.Callback ************************************************************
override fun onUrlClicked(url: String): Boolean { override fun onUrlClicked(url: String): Boolean {
val managed = permalinkHandler.launch(requireActivity(), url, object : NavigateToRoomInterceptor { permalinkHandler
override fun navToRoom(roomId: String, eventId: String?): Boolean { .launch(requireActivity(), url, object : NavigateToRoomInterceptor {
// Same room? override fun navToRoom(roomId: String?, eventId: String?): Boolean {
if (roomId == roomDetailArgs.roomId) { // Same room?
// Navigation to same room if (roomId == roomDetailArgs.roomId) {
if (eventId == null) { // Navigation to same room
showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room)) if (eventId == null) {
} else { showSnackWithMessage(getString(R.string.navigate_to_room_when_already_in_the_room))
// Highlight and scroll to this event } else {
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true)) // Highlight and scroll to this event
roomDetailViewModel.handle(RoomDetailAction.NavigateToEvent(eventId, true))
}
return true
}
// Not handled
return false
}
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { managed ->
if (!managed) {
// Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url)
} }
return true
} }
.disposeOnDestroyView()
// Not handled
return false
}
})
if (!managed) {
// Open in external browser, in a new Tab
openUrlInExternalBrowser(requireContext(), url)
}
// In fact it is always managed // In fact it is always managed
return true return true
} }
@ -1025,12 +1027,15 @@ class RoomDetailFragment @Inject constructor(
} }
override fun onRoomCreateLinkClicked(url: String) { override fun onRoomCreateLinkClicked(url: String) {
permalinkHandler.launch(requireContext(), url, object : NavigateToRoomInterceptor { permalinkHandler
override fun navToRoom(roomId: String, eventId: String?): Boolean { .launch(requireContext(), url, object : NavigateToRoomInterceptor {
requireActivity().finish() override fun navToRoom(roomId: String?, eventId: String?): Boolean {
return false requireActivity().finish()
} return false
}) }
})
.subscribe()
.disposeOnDestroyView()
} }
override fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) { override fun onReadReceiptsClicked(readReceipts: List<ReadReceiptData>) {
@ -1197,9 +1202,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

@ -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)

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