Merge branch 'release/0.11.0'

This commit is contained in:
Benoit Marty 2019-12-19 16:44:27 +01:00
commit 358fcb6b34
231 changed files with 7373 additions and 1795 deletions

View File

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

View File

@ -1,3 +1,27 @@
Changes in RiotX 0.11.0 (2019-12-19)
===================================================
Features ✨:
- Implement soft logout (#281)
Improvements 🙌:
- Handle navigation to room via room alias (#201)
- Open matrix.to link in RiotX (#57)
- Limit sticker size in the timeline
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
- Fix rendering issue with HTML formatted body
- Disable click on Stickers (#703)
Build 🧱:
- Include diff-match-patch sources as dependency
Changes in RiotX 0.10.0 (2019-12-10)
===================================================

View File

@ -10,7 +10,7 @@ buildscript {
}
}
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.airbnb.okreplay:gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
@ -45,12 +45,6 @@ allprojects {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
google()
jcenter()
maven {
url 'https://repo.adobe.com/nexus/content/repositories/public/'
content {
includeGroupByRegex "diff_match_patch"
}
}
}
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()
}
fun loadRoomMembersIfNeeded(): Single<Unit> = Single.create {
room.loadRoomMembersIfNeeded(MatrixCallbackSingle(it)).toSingle(it)
fun loadRoomMembersIfNeeded(): Single<Unit> = singleBuilder {
room.loadRoomMembersIfNeeded(it)
}
fun joinRoom(reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
room.join(reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
room.join(reason, viaServers, it)
}
fun liveEventReadReceipts(eventId: String): Observable<List<ReadReceipt>> {

View File

@ -66,20 +66,25 @@ class RxSession(private val session: Session) {
return session.livePagedUsers(filter).asObservable()
}
fun createRoom(roomParams: CreateRoomParams): Single<String> = Single.create {
session.createRoom(roomParams, MatrixCallbackSingle(it)).toSingle(it)
fun createRoom(roomParams: CreateRoomParams): Single<String> = singleBuilder {
session.createRoom(roomParams, it)
}
fun searchUsersDirectory(search: String,
limit: Int,
excludedUserIds: Set<String>): Single<List<User>> = Single.create {
session.searchUsersDirectory(search, limit, excludedUserIds, MatrixCallbackSingle(it)).toSingle(it)
excludedUserIds: Set<String>): Single<List<User>> = singleBuilder {
session.searchUsersDirectory(search, limit, excludedUserIds, it)
}
fun joinRoom(roomId: String,
reason: String? = null,
viaServers: List<String> = emptyList()): Single<Unit> = Single.create {
session.joinRoom(roomId, reason, viaServers, MatrixCallbackSingle(it)).toSingle(it)
viaServers: List<String> = emptyList()): Single<Unit> = singleBuilder {
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(
val credentials: Credentials,
val homeServerConnectionConfig: HomeServerConnectionConfig
val homeServerConnectionConfig: HomeServerConnectionConfig,
val isTokenValid: Boolean
)

View File

@ -0,0 +1,23 @@
/*
* 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.failure
// This class will be sent to the bus
sealed class GlobalError {
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.
* You shouldn't have to instantiate it.
* Ref: https://matrix.org/docs/spec/client_server/latest#api-standards
*/
@JsonClass(generateAdapter = true)
data class MatrixError(
/** unique string which can be used to handle an error message */
@Json(name = "errcode") val code: String,
/** human-readable error message */
@Json(name = "error") val message: String,
// For M_CONSENT_NOT_GIVEN
@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 = "admin_contact") val adminUri: String? = null,
// For LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null) {
// For M_LIMIT_EXCEEDED
@Json(name = "retry_after_ms") val retryAfterMillis: Long? = null,
// For M_UNKNOWN_TOKEN
@Json(name = "soft_logout") val isSoftLogout: Boolean = false
) {
companion object {
const val FORBIDDEN = "M_FORBIDDEN"
const val UNKNOWN = "M_UNKNOWN"
const val UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
const val MISSING_TOKEN = "M_MISSING_TOKEN"
const val BAD_JSON = "M_BAD_JSON"
const val NOT_JSON = "M_NOT_JSON"
const val NOT_FOUND = "M_NOT_FOUND"
const val LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
const val USER_IN_USE = "M_USER_IN_USE"
const val ROOM_IN_USE = "M_ROOM_IN_USE"
const val BAD_PAGINATION = "M_BAD_PAGINATION"
const val UNAUTHORIZED = "M_UNAUTHORIZED"
const val OLD_VERSION = "M_OLD_VERSION"
const val UNRECOGNIZED = "M_UNRECOGNIZED"
/** Forbidden access, e.g. joining a room without permission, failed login. */
const val M_FORBIDDEN = "M_FORBIDDEN"
/** An unknown error has occurred. */
const val M_UNKNOWN = "M_UNKNOWN"
/** The access token specified was not recognised. */
const val M_UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
/** No access token was specified for the request. */
const val M_MISSING_TOKEN = "M_MISSING_TOKEN"
/** Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys. */
const val M_BAD_JSON = "M_BAD_JSON"
/** Request did not contain valid JSON. */
const val M_NOT_JSON = "M_NOT_JSON"
/** No resource was found for this request. */
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"
// 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"
const val SERVER_NOT_TRUSTED = "M_SERVER_NOT_TRUSTED"
const val TOO_LARGE = "M_TOO_LARGE"
/* ==========================================================================================
* Other error codes the client might encounter are
* ========================================================================================== */
/** Encountered when trying to register a user ID which has been taken. */
const val M_USER_IN_USE = "M_USER_IN_USE"
/** 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 RESOURCE_LIMIT_EXCEEDED = "M_RESOURCE_LIMIT_EXCEEDED"
const val WRONG_ROOM_KEYS_VERSION = "M_WRONG_ROOM_KEYS_VERSION"
/** The request cannot be completed because the homeserver has reached a resource limit imposed on it. For example,
* 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"
const val LIMIT_TYPE_MAU = "monthly_active_user"

View File

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

View File

@ -60,16 +60,21 @@ object PermalinkParser {
return PermalinkData.FallbackLink(uri)
}
return when {
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
if (!extraParameter.isNullOrEmpty() && MatrixPatterns.isEventId(extraParameter)) {
PermalinkData.EventLink(roomIdOrAlias = identifier, eventId = extraParameter)
} else {
PermalinkData.RoomLink(roomIdOrAlias = identifier)
MatrixPatterns.isUserId(identifier) -> PermalinkData.UserLink(userId = identifier)
MatrixPatterns.isGroupId(identifier) -> PermalinkData.GroupLink(groupId = identifier)
MatrixPatterns.isRoomId(identifier) -> {
val eventId = extraParameter.takeIf {
!it.isNullOrEmpty() && MatrixPatterns.isEventId(it)
}
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.lifecycle.LiveData
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.session.cache.CacheService
import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
@ -62,6 +62,11 @@ interface Session :
*/
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
*/
@ -81,7 +86,7 @@ interface Session :
/**
* 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 :/
*/
fun startAutomaticBackgroundSync(repeatDelay: Long = 30_000L)
@ -136,13 +141,10 @@ interface Session :
*/
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()
/**
* A M_CONSENT_NOT_GIVEN error has been received from the homeserver
*/
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError)
fun onGlobalError(globalError: GlobalError)
}
}

View File

@ -30,7 +30,7 @@ data class ContentAttachmentData(
val exifOrientation: Int = ExifInterface.ORIENTATION_UNDEFINED,
val name: String? = null,
val path: String,
val mimeType: String,
val mimeType: String?,
val type: Type
) : Parcelable {

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.create.CreateRoomParams
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.
@ -74,4 +75,11 @@ interface RoomService {
*/
fun markAllAsRead(roomIds: List<String>,
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.
*/
fun join(reason: String? = null,
viaServers: List<String> = emptyList(),
callback: MatrixCallback<Unit>): Cancelable

View File

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

View File

@ -25,7 +25,7 @@ data class VideoInfo(
/**
* The mimetype of the video e.g. "video/mp4".
*/
@Json(name = "mimetype") val mimeType: String,
@Json(name = "mimetype") val mimeType: String?,
/**
* The width of the video in pixels.

View File

@ -16,11 +16,12 @@
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.
* These Spans will be transformed into pills when detected in message to send
*/
interface UserMentionSpan {
val displayName: String
val userId: String
val matrixItem: MatrixItem
}

View File

@ -104,7 +104,7 @@ fun TimelineEvent.getLastMessageContent(): MessageContent? {
root.getClearContent().toModel<MessageStickerContent>()
} else {
annotations?.editSummary?.aggregatedContent?.toModel()
?: root.getClearContent().toModel()
?: root.getClearContent().toModel()
}
}
@ -116,7 +116,7 @@ fun TimelineEvent.getLastMessageBody(): String? {
if (lastMessageContent != null) {
return lastMessageContent.newContent?.toModel<MessageContent>()?.body
?: lastMessageContent.body
?: lastMessageContent.body
}
return null

View File

@ -17,14 +17,31 @@
package im.vector.matrix.android.api.session.signout
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 {
/**
* 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
sealed class SyncState {
object IDLE : SyncState()
data class RUNNING(val afterPause: Boolean) : SyncState()
object PAUSED : SyncState()
object KILLING : SyncState()
object KILLED : SyncState()
object NO_NETWORK : SyncState()
object Idle : SyncState()
data class Running(val afterPause: Boolean) : SyncState()
object Paused : SyncState()
object Killing : SyncState()
object Killed : 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")
.modules(AuthRealmModule())
.schemaVersion(AuthRealmMigration.SCHEMA_VERSION)
.migration(AuthRealmMigration())
.migration(AuthRealmMigration)
.build()
}
}

View File

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

View File

@ -16,6 +16,7 @@
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
internal interface SessionParamsStore {
@ -28,6 +29,10 @@ internal interface SessionParamsStore {
suspend fun save(sessionParams: SessionParams)
suspend fun setTokenInvalid(userId: String)
suspend fun updateCredentials(newCredentials: Credentials)
suspend fun delete(userId: String)
suspend fun deleteAll()

View File

@ -20,12 +20,10 @@ import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
internal class AuthRealmMigration : RealmMigration {
internal object AuthRealmMigration : RealmMigration {
companion object {
// Current schema version
const val SCHEMA_VERSION = 1L
}
// Current schema version
const val SCHEMA_VERSION = 2L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
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.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
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.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) {
awaitTransaction(realmConfiguration) {
it.where(SessionParamsEntity::class.java)

View File

@ -22,5 +22,8 @@ import io.realm.annotations.PrimaryKey
internal open class SessionParamsEntity(
@PrimaryKey var userId: 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()

View File

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

View File

@ -49,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String?): EncryptionResult {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()

View File

@ -807,7 +807,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
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
callback.onSuccess(null)
} else {
@ -830,7 +830,7 @@ internal class KeysBackup @Inject constructor(
override fun onFailure(failure: Throwable) {
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
callback.onSuccess(null)
} else {
@ -1209,8 +1209,8 @@ internal class KeysBackup @Inject constructor(
Timber.e(failure, "backupKeys: backupKeys failed.")
when (failure.error.code) {
MatrixError.NOT_FOUND,
MatrixError.WRONG_ROOM_KEYS_VERSION -> {
MatrixError.M_NOT_FOUND,
MatrixError.M_WRONG_ROOM_KEYS_VERSION -> {
// Backup has been deleted on the server, or we are not using the last backup version
keysBackupStateManager.state = KeysBackupState.WrongBackUpVersion
backupAllGroupSessionsCallback?.onFailure(failure)

View File

@ -68,7 +68,9 @@ internal class RoomSummaryMapper @Inject constructor(
membership = roomSummaryEntity.membership,
versioningState = roomSummaryEntity.versioningState,
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 tags: RealmList<RoomTagEntity> = RealmList(),
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() {
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
}
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 {
return where(realm, roomId).findFirst() ?: realm.createObject(roomId)
}

View File

@ -16,19 +16,29 @@
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.Response
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 {
var request = chain.request()
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer " + credentials.accessToken)
request = newRequestBuilder.build()
accessToken?.let {
val newRequestBuilder = request.newBuilder()
// Add the access token to all requests if it is set
newRequestBuilder.addHeader(HttpHeaders.Authorization, "Bearer $it")
request = newRequestBuilder.build()
}
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.JsonEncodingException
import im.vector.matrix.android.api.failure.ConsentNotGivenError
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.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
@ -32,6 +32,7 @@ import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import java.net.HttpURLConnection
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@ -99,7 +100,11 @@ private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (matrixError != null) {
if (matrixError.code == MatrixError.M_CONSENT_NOT_GIVEN && !matrixError.consentUri.isNullOrBlank()) {
// 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)

View File

@ -83,15 +83,13 @@ internal class DefaultFileService @Inject constructor(private val context: Conte
if (elementToDecrypt != null) {
Timber.v("## decrypt file")
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt) ?: throw IllegalStateException("Decryption error")
} else {
inputStream
MXEncryptedAttachments.decryptAttachment(inputStream, elementToDecrypt)
?: throw IllegalStateException("Decryption error")
}
writeToFile(inputStream, destFile)
destFile
}
.map { inputStream ->
writeToFile(inputStream, destFile)
destFile
}
} else {
Try.just(destFile)
}

View File

@ -23,7 +23,7 @@ import androidx.lifecycle.LiveData
import dagger.Lazy
import im.vector.matrix.android.api.MatrixCallback
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.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
@ -42,10 +42,14 @@ import im.vector.matrix.android.api.session.signout.SignOutService
import im.vector.matrix.android.api.session.sync.FilterService
import im.vector.matrix.android.api.session.sync.SyncState
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.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.sync.job.SyncThread
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.Subscribe
import org.greenrobot.eventbus.ThreadMode
@ -72,6 +76,7 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val secureStorageService: Lazy<SecureStorageService>,
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val sessionParamsStore: SessionParamsStore,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
@ -94,6 +99,9 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private var syncThread: SyncThread? = null
override val isOpenable: Boolean
get() = sessionParamsStore.get(myUserId)?.isTokenValid ?: false
@MainThread
override fun open() {
assertMainThread()
@ -170,8 +178,16 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onConsentNotGivenError(consentNotGivenError: ConsentNotGivenError) {
sessionListeners.dispatchConsentNotGiven(consentNotGivenError)
fun onGlobalError(globalError: GlobalError) {
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

View File

@ -16,7 +16,7 @@
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 javax.inject.Inject
@ -36,10 +36,10 @@ internal class SessionListeners @Inject constructor() {
}
}
fun dispatchConsentNotGiven(consentNotGivenError: ConsentNotGivenError) {
fun dispatchGlobalError(globalError: GlobalError) {
synchronized(listeners) {
listeners.forEach {
it.onConsentNotGivenError(consentNotGivenError)
it.onGlobalError(globalError)
}
}
}

View File

@ -43,9 +43,9 @@ internal class FileUploader @Inject constructor(@Authenticated
suspend fun uploadFile(file: File,
filename: String?,
mimeType: String,
mimeType: String?,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = file.asRequestBody(mimeType.toMediaTypeOrNull())
val uploadBody = file.asRequestBody(mimeType?.toMediaTypeOrNull())
return upload(uploadBody, filename, progressListener)
}

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.create.CreateRoomParams
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.model.RoomEntity
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.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.membership.joining.JoinRoomTask
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 markAllRoomsReadTask: MarkAllRoomsReadTask,
private val updateBreadcrumbsTask: UpdateBreadcrumbsTask,
private val roomIdByAliasTask: GetRoomIdByAliasTask,
private val roomFactory: RoomFactory,
private val taskExecutor: TaskExecutor) : RoomService {
@ -111,4 +114,12 @@ internal class DefaultRoomService @Inject constructor(private val monarchy: Mona
}
.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.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.CreateRoomResponse
import im.vector.matrix.android.api.session.room.model.roomdirectory.PublicRoomsParams
@ -258,4 +259,12 @@ internal interface RoomAPI {
fun reportContent(@Path("roomId") roomId: String,
@Path("eventId") eventId: String,
@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.internal.session.DefaultFileService
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.DefaultCreateRoomTask
import im.vector.matrix.android.internal.session.room.directory.DefaultGetPublicRoomTask
@ -133,4 +135,7 @@ internal abstract class RoomModule {
@Binds
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.toModel
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.internal.database.mapper.ContentMapper
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,
updateMembers: Boolean = false) {
val roomSummaryEntity = RoomSummaryEntity.where(realm, roomId).findFirst()
?: realm.createObject(roomId)
?: realm.createObject(roomId)
if (roomSummary != null) {
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 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
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
// avoid this call if we are sure there are unread events
|| !isEventRead(monarchy, userId, roomId, latestPreviewableEvent?.eventId)
roomSummaryEntity.displayName = roomDisplayNameResolver.resolve(roomId).toString()
roomSummaryEntity.avatarUrl = roomAvatarResolver.resolve(roomId)
roomSummaryEntity.topic = ContentMapper.map(lastTopicEvent?.content).toModel<RoomTopicContent>()?.topic
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) {
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

@ -52,7 +52,7 @@ internal class DefaultCreateRoomTask @Inject constructor(private val roomAPI: Ro
apiCall = roomAPI.createRoom(params)
}
val roomId = createRoomResponse.roomId!!
// Wait for room to come back from the sync (but it can maybe be in the DB is the sync response is received before)
// Wait for room to come back from the sync (but it can maybe be in the DB if the sync response is received before)
val rql = RealmQueryLatch<RoomEntity>(realmConfiguration) { realm ->
realm.where(RoomEntity::class.java)
.equalTo(RoomEntityFields.ROOM_ID, roomId)

View File

@ -251,7 +251,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_AUDIO,
body = attachment.name ?: "audio",
audioInfo = AudioInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() } ?: "audio/mpeg",
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() } ?: "audio/mpeg",
size = attachment.size
),
url = attachment.path
@ -264,7 +264,7 @@ internal class LocalEchoEventFactory @Inject constructor(
type = MessageType.MSGTYPE_FILE,
body = attachment.name ?: "file",
info = FileInfo(
mimeType = attachment.mimeType.takeIf { it.isNotBlank() }
mimeType = attachment.mimeType?.takeIf { it.isNotBlank() }
?: "application/octet-stream",
size = attachment.size
),

View File

@ -79,7 +79,7 @@ internal class SendEventWorker constructor(context: Context, params: WorkerParam
private fun Throwable.shouldBeRetried(): Boolean {
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) {

View File

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

View File

@ -17,17 +17,43 @@
package im.vector.matrix.android.internal.session.signout
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.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.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
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 {
override fun signOut(callback: MatrixCallback<Unit>) {
signOutTask
.configureWith {
override fun signInAgain(password: String,
callback: MatrixCallback<Unit>): Cancelable {
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
}
.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
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 retrofit2.Call
import retrofit2.http.Body
import retrofit2.http.Headers
import retrofit2.http.POST
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.
*/

View File

@ -37,8 +37,11 @@ internal abstract class SignOutModule {
}
@Binds
abstract fun bindSignOutTask(signOutTask: DefaultSignOutTask): SignOutTask
abstract fun bindSignOutTask(task: DefaultSignOutTask): SignOutTask
@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 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.auth.SessionParamsStore
import im.vector.matrix.android.internal.crypto.CryptoModule
@ -32,9 +34,14 @@ import io.realm.Realm
import io.realm.RealmConfiguration
import timber.log.Timber
import java.io.File
import java.net.HttpURLConnection
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,
@UserId private val userId: String,
@ -49,10 +56,26 @@ internal class DefaultSignOutTask @Inject constructor(private val context: Conte
@CryptoDatabase private val realmCryptoConfiguration: RealmConfiguration,
@UserMd5 private val userMd5: String) : SignOutTask {
override suspend fun execute(params: Unit) {
Timber.d("SignOut: send request...")
executeRequest<Unit> {
apiCall = signOutAPI.signOut()
override suspend fun execute(params: SignOutTask.Params) {
// It should be done even after a soft logout, to be sure the deviceId is deleted on the
if (params.sigOutFromHomeserver) {
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...")

View File

@ -17,8 +17,6 @@
package im.vector.matrix.android.internal.session.sync
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.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
@ -67,17 +65,8 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
initialSyncProgressService.endAll()
initialSyncProgressService.startTask(R.string.initial_sync_start_importing_account, 100)
}
val syncResponse = try {
executeRequest<SyncResponse> {
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
val syncResponse = executeRequest<SyncResponse> {
apiCall = syncAPI.sync(requestParams)
}
syncResponseHandler.handleResponse(syncResponse, token)
syncTokenStore.saveToken(syncResponse.nextBatch)

View File

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

View File

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

View File

@ -16,9 +16,7 @@
package im.vector.matrix.android.internal.util
import im.vector.matrix.android.api.MatrixPatterns
import timber.log.Timber
import java.util.Locale
/**
* Convert a string to an UTF8 String
@ -51,10 +49,3 @@ fun convertFromUTF8(s: String): String {
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

@ -82,4 +82,86 @@
<string name="room_displayname_empty_room">Prázdná místnost</string>
<string name="notice_room_update">%s upravil/a tuto místnost.</string>
<string name="notice_event_redacted_with_reason">Zpráva byla smazána [důvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Zpráva smazána [smazal/a %1$s] [důvod: %2$s]</string>
<string name="notice_room_third_party_revoked_invite">"%1$s obnovil/a pozvánku do místnosti pro %2$s"</string>
<string name="verification_emoji_cat">Kočka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_horse">Kůň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prase</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Králík</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rooster">Kohout</string>
<string name="verification_emoji_penguin">Tučnák</string>
<string name="verification_emoji_turtle">Želva</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnice</string>
<string name="verification_emoji_butterfly">Motýl</string>
<string name="verification_emoji_flower">Květina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Houba</string>
<string name="verification_emoji_globe">Glóbus</string>
<string name="verification_emoji_moon">Měsíc</string>
<string name="verification_emoji_cloud">Mrak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukuřice</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Dort</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Smajlík</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobouk</string>
<string name="verification_emoji_glasses">Brýle</string>
<string name="verification_emoji_santa">Santa</string>
<string name="verification_emoji_thumbsup">Zvednutý palec</string>
<string name="verification_emoji_umbrella">Deštník</string>
<string name="verification_emoji_hourglass">Přesípací hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Dárek</string>
<string name="verification_emoji_lightbulb">Žárovka</string>
<string name="verification_emoji_book">Knížka</string>
<string name="verification_emoji_pencil">Tužka</string>
<string name="verification_emoji_paperclip">Sponka</string>
<string name="verification_emoji_scissors">Nůžky</string>
<string name="verification_emoji_lock">Zámek</string>
<string name="verification_emoji_key">Klíč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefon</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_bicycle">Kolo</string>
<string name="verification_emoji_airplane">Letadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Pohár</string>
<string name="verification_emoji_ball">Míč</string>
<string name="verification_emoji_guitar">Kytara</string>
<string name="verification_emoji_trumpet">Trumpeta</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Sluchátka</string>
<string name="verification_emoji_folder">Složka</string>
<string name="initial_sync_start_importing_account">Úvodní synchronizace:
\nStahuji účet…</string>
<string name="initial_sync_start_importing_account_crypto">Uvodní synchronizace:
\nStahuji klíče</string>
<string name="initial_sync_start_importing_account_rooms">Uvodní synchnizace:
\nStahuji místnost</string>
<string name="initial_sync_start_importing_account_joined_rooms">Uvodní synchronizace:
\nStahuji moje místnosti</string>
<string name="initial_sync_start_importing_account_left_rooms">Uvodní synchonizace:
\nStahuji místnosti, které jsem opustil/a</string>
<string name="initial_sync_start_importing_account_groups">Úvodní sychronizace:
\nImportuji komunity</string>
<string name="initial_sync_start_importing_account_data">Úvodní synchronizace:
\nImportuji data účtu</string>
<string name="event_status_sending_message">Posílám zprávu…</string>
</resources>

View File

@ -49,7 +49,7 @@
<string name="notice_room_third_party_invite">%1$s님이 %2$s님에게 방 초대를 보냈습니다</string>
<string name="notice_room_third_party_registered_invite">%1$s님이 %2$s의 초대를 수락했습니다</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 해독할 수 없음: %s **</string>
<string name="notice_crypto_unable_to_decrypt">** 암호를 복호화할 수 없음: %s **</string>
<string name="notice_crypto_error_unkwown_inbound_session_id">발신인의 기기에서 이 메시지의 키를 보내지 않았습니다.</string>
<string name="message_reply_to_prefix">관련 대화</string>

View File

@ -88,70 +88,70 @@
<string name="notice_event_redacted_by">Správa odstránená používateľom %1$s</string>
<string name="notice_event_redacted_with_reason">Správa odstránená [dôvod: %1$s]</string>
<string name="notice_event_redacted_by_with_reason">Správa odstránená používateľom %1$s [dôvod: %2$s]</string>
<string name="verification_emoji_dog">Pes</string>
<string name="verification_emoji_cat">Mačka</string>
<string name="verification_emoji_lion">Lev</string>
<string name="verification_emoji_dog">Hlava psa</string>
<string name="verification_emoji_cat">Hlava mačky</string>
<string name="verification_emoji_lion">Hlava leva</string>
<string name="verification_emoji_horse">Kôň</string>
<string name="verification_emoji_unicorn">Jednorožec</string>
<string name="verification_emoji_pig">Prasa</string>
<string name="verification_emoji_unicorn">Hlava jednorožca</string>
<string name="verification_emoji_pig">Hlava prasaťa</string>
<string name="verification_emoji_elephant">Slon</string>
<string name="verification_emoji_rabbit">Zajac</string>
<string name="verification_emoji_panda">Panda</string>
<string name="verification_emoji_rabbit">Hlava zajaca</string>
<string name="verification_emoji_panda">Hlava pandy</string>
<string name="verification_emoji_rooster">Kohút</string>
<string name="verification_emoji_penguin">Tučniak</string>
<string name="verification_emoji_turtle">Korytnačka</string>
<string name="verification_emoji_fish">Ryba</string>
<string name="verification_emoji_octopus">Chobotnica</string>
<string name="verification_emoji_butterfly">Motýľ</string>
<string name="verification_emoji_flower">Kvetina</string>
<string name="verification_emoji_tree">Strom</string>
<string name="verification_emoji_flower">Tulipán</string>
<string name="verification_emoji_tree">Listnatý strom</string>
<string name="verification_emoji_cactus">Kaktus</string>
<string name="verification_emoji_mushroom">Hríb</string>
<string name="verification_emoji_mushroom">Huba</string>
<string name="verification_emoji_globe">Zemeguľa</string>
<string name="verification_emoji_moon">Mesiac</string>
<string name="verification_emoji_moon">Polmesiac</string>
<string name="verification_emoji_cloud">Oblak</string>
<string name="verification_emoji_fire">Oheň</string>
<string name="verification_emoji_banana">Banán</string>
<string name="verification_emoji_apple">Jablko</string>
<string name="verification_emoji_apple">Červené jablko</string>
<string name="verification_emoji_strawberry">Jahoda</string>
<string name="verification_emoji_corn">Kukurica</string>
<string name="verification_emoji_corn">Kukuričný klas</string>
<string name="verification_emoji_pizza">Pizza</string>
<string name="verification_emoji_cake">Koláč</string>
<string name="verification_emoji_heart">Srdce</string>
<string name="verification_emoji_smiley">Úsmev</string>
<string name="verification_emoji_cake">Narodeninová torta</string>
<string name="verification_emoji_heart">Červené</string>
<string name="verification_emoji_smiley">Škeriaca sa tvár</string>
<string name="verification_emoji_robot">Robot</string>
<string name="verification_emoji_hat">Klobúk</string>
<string name="verification_emoji_hat">Cylinder</string>
<string name="verification_emoji_glasses">Okuliare</string>
<string name="verification_emoji_wrench">Skrutkovač</string>
<string name="verification_emoji_santa">Mikuláš</string>
<string name="verification_emoji_wrench">Francúzsky kľúč</string>
<string name="verification_emoji_santa">Santa Claus</string>
<string name="verification_emoji_thumbsup">Palec nahor</string>
<string name="verification_emoji_umbrella">Dáždnik</string>
<string name="verification_emoji_hourglass">Presýpacie hodiny</string>
<string name="verification_emoji_clock">Hodiny</string>
<string name="verification_emoji_gift">Darček</string>
<string name="verification_emoji_clock">Budík</string>
<string name="verification_emoji_gift">Zabalený darček</string>
<string name="verification_emoji_lightbulb">Žiarovka</string>
<string name="verification_emoji_book">Kniha</string>
<string name="verification_emoji_book">Zatvorená kniha</string>
<string name="verification_emoji_pencil">Ceruzka</string>
<string name="verification_emoji_paperclip">Kancelárska sponka</string>
<string name="verification_emoji_paperclip">Sponka na papier</string>
<string name="verification_emoji_scissors">Nožnice</string>
<string name="verification_emoji_lock">Zámok</string>
<string name="verification_emoji_lock">Zatvorená zámka</string>
<string name="verification_emoji_key">Kľúč</string>
<string name="verification_emoji_hammer">Kladivo</string>
<string name="verification_emoji_telephone">Telefón</string>
<string name="verification_emoji_flag">Vlajka</string>
<string name="verification_emoji_train">Vlak</string>
<string name="verification_emoji_flag">Kockovaná zástava</string>
<string name="verification_emoji_train">Rušeň</string>
<string name="verification_emoji_bicycle">Bicykel</string>
<string name="verification_emoji_airplane">Lietadlo</string>
<string name="verification_emoji_rocket">Raketa</string>
<string name="verification_emoji_trophy">Trofej</string>
<string name="verification_emoji_ball">Lopta</string>
<string name="verification_emoji_ball">Futbal</string>
<string name="verification_emoji_guitar">Gitara</string>
<string name="verification_emoji_trumpet">Trúbka</string>
<string name="verification_emoji_bell">Zvonček</string>
<string name="verification_emoji_bell">Zvon</string>
<string name="verification_emoji_anchor">Kotva</string>
<string name="verification_emoji_headphone">Schlúchadlá</string>
<string name="verification_emoji_folder">Priečinok</string>
<string name="verification_emoji_pin">Pin</string>
<string name="verification_emoji_headphone">Slúchadlá</string>
<string name="verification_emoji_folder">Fascikel</string>
<string name="verification_emoji_pin">Špendlík</string>
<string name="initial_sync_start_importing_account">Úvodná synchronizácia:
\nPrebieha import účtu…</string>
@ -173,4 +173,5 @@
<string name="event_status_sending_message">Odosielanie správy…</string>
<string name="clear_timeline_send_queue">Vymazať správy na odoslanie</string>
<string name="notice_room_third_party_revoked_invite">%1$s zamietol pozvanie používateľa %2$s vstúpiť do miestnosti</string>
</resources>

View File

@ -13,7 +13,7 @@
<string name="notice_room_ban">%1$s 封禁了 %2$s</string>
<string name="notice_avatar_url_changed">%1$s 更换了他们的头像</string>
<string name="notice_display_name_set">%1$s 将他们的昵称设置为 %2$s</string>
<string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string>
<string name="notice_display_name_changed_from">%1$s 把他的昵称从 %2$s 改为 %3$s</string>
<string name="notice_display_name_removed">%1$s 移除了他们的昵称 (%2$s)</string>
<string name="notice_room_topic_changed">%1$s 把主题改为: %2$s</string>
<string name="notice_room_name_changed">%1$s 把聊天室名称改为: %2$s</string>
@ -167,4 +167,7 @@
<string name="event_status_sending_message">正在发送消息…</string>
<string name="clear_timeline_send_queue">清除正在发送队列</string>
<string name="notice_room_third_party_revoked_invite">%1$s 撤回了对 %2$s 邀请</string>
<string name="verification_emoji_pin">置顶</string>
</resources>

View File

@ -243,4 +243,18 @@
<string name="event_status_sending_message">Sending message…</string>
<string name="clear_timeline_send_queue">Clear sending queue</string>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
</resources>

View File

@ -2,19 +2,6 @@
<resources>
<string name="notice_room_invite_no_invitee_with_reason">%1$s\'s invitation. Reason: %2$s</string>
<string name="notice_room_invite_with_reason">%1$s invited %2$s. Reason: %3$s</string>
<string name="notice_room_invite_you_with_reason">%1$s invited you. Reason: %2$s</string>
<string name="notice_room_join_with_reason">%1$s joined. Reason: %2$s</string>
<string name="notice_room_leave_with_reason">%1$s left. Reason: %2$s</string>
<string name="notice_room_reject_with_reason">%1$s rejected the invitation. Reason: %2$s</string>
<string name="notice_room_kick_with_reason">%1$s kicked %2$s. Reason: %3$s</string>
<string name="notice_room_unban_with_reason">%1$s unbanned %2$s. Reason: %3$s</string>
<string name="notice_room_ban_with_reason">%1$s banned %2$s. Reason: %3$s</string>
<string name="notice_room_third_party_invite_with_reason">%1$s sent an invitation to %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_revoked_invite_with_reason">%1$s revoked the invitation for %2$s to join the room. Reason: %3$s</string>
<string name="notice_room_third_party_registered_invite_with_reason">%1$s accepted the invitation for %2$s. Reason: %3$s</string>
<string name="notice_room_withdraw_with_reason">%1$s withdrew %2$s\'s invitation. Reason: %3$s</string>
<string name="no_network_indicator">There is no network connection right now</string>
</resources>

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.versionMinor = 10
ext.versionMinor = 11
ext.versionPatch = 0
static def getGitTimestamp() {
@ -229,6 +229,7 @@ dependencies {
implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx")
implementation project(":diff-match-patch")
implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
@ -292,6 +293,7 @@ dependencies {
implementation 'me.gujun.android:span:1.7'
implementation "io.noties.markwon:core:$markwon_version"
implementation "io.noties.markwon:html:$markwon_version"
implementation 'com.googlecode.htmlcompressor:htmlcompressor:1.4'
implementation 'me.saket:better-link-movement-method:2.2.0'
implementation 'com.google.android:flexbox:1.1.1'
implementation "androidx.autofill:autofill:$autofill_version"
@ -341,8 +343,6 @@ dependencies {
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"
// TESTS

View File

@ -31,4 +31,8 @@
<issue id="ViewConstructor" severity="error" />
<issue id="UseValueOf" severity="error" />
<!-- Ignore error from HtmlCompressor lib -->
<issue id="InvalidPackage">
<ignore path="**/htmlcompressor-1.4.jar"/>
</issue>
</lint>

View File

@ -65,7 +65,13 @@
<activity android:name=".features.roomdirectory.RoomDirectoryActivity" />
<activity android:name=".features.roomdirectory.roompreview.RoomPreviewActivity" />
<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.home.createdirect.CreateDirectRoomActivity" />
<activity android:name=".features.webview.VectorWebViewActivity" />
@ -98,6 +104,22 @@
<category android:name="android.intent.category.OPENABLE" />
</intent-filter>
</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 -->
<service

View File

@ -359,6 +359,11 @@ SOFTWARE.
<br/>
Copyright 2018 Kumar Bibek
</li>
<li>
<b>htmlcompressor</b>
<br/>
Copyright 2017 Sergiy Kovalchuk
</li>
</ul>
<pre>
Apache License

View File

@ -37,7 +37,7 @@ class ActiveSessionHolder @Inject constructor(private val authenticationService:
fun setActiveSession(session: Session) {
activeSession.set(session)
sessionObservableStore.post(Option.fromNullable(session))
sessionObservableStore.post(Option.just(session))
keyRequestHandler.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.ignored.VectorSettingsIgnoredUsersFragment
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.signout.soft.SoftLogoutFragment
@Module
interface FragmentModule {
@ -261,4 +262,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(EmojiChooserFragment::class)
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 dagger.BindsInstance
import dagger.Component
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.preference.UserAvatarPreference
import im.vector.riotx.features.MainActivity
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.VideoMediaViewerActivity
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.BugReporter
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.settings.VectorSettingsActivity
import im.vector.riotx.features.share.IncomingShareActivity
import im.vector.riotx.features.signout.soft.SoftLogoutActivity
import im.vector.riotx.features.ui.UiStateRepository
@Component(
@ -78,6 +81,8 @@ interface ScreenComponent {
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity)
@ -126,6 +131,10 @@ interface ScreenComponent {
fun inject(roomListActionsBottomSheet: RoomListQuickActionsBottomSheet)
fun inject(activity: SoftLogoutActivity)
fun inject(permalinkHandlerActivity: PermalinkHandlerActivity)
@Component.Factory
interface Factory {
fun create(vectorComponent: VectorComponent,

View File

@ -27,6 +27,7 @@ import im.vector.riotx.ActiveSessionDataSource
import im.vector.riotx.EmojiCompatFontProvider
import im.vector.riotx.EmojiCompatWrapper
import im.vector.riotx.VectorApplication
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.pushers.PushersManager
import im.vector.riotx.core.utils.AssetReader
import im.vector.riotx.core.utils.DimensionConverter
@ -37,6 +38,7 @@ import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.HomeRoomListDataSource
import im.vector.riotx.features.home.group.SelectedGroupDataSource
import im.vector.riotx.features.html.EventHtmlRenderer
import im.vector.riotx.features.html.VectorHtmlCompressor
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.notifications.*
import im.vector.riotx.features.rageshake.BugReporter
@ -86,8 +88,12 @@ interface VectorComponent {
fun eventHtmlRenderer(): EventHtmlRenderer
fun vectorHtmlCompressor(): VectorHtmlCompressor
fun navigator(): Navigator
fun errorFormatter(): ErrorFormatter
fun homeRoomListObservableStore(): HomeRoomListDataSource
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.auth.AuthenticationService
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.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
@ -72,6 +74,9 @@ abstract class VectorModule {
@Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindErrorFormatter(errorFormatter: DefaultErrorFormatter): ErrorFormatter
@Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
}

View File

@ -14,25 +14,17 @@
* limitations under the License.
*/
package im.vector.matrix.rx
package im.vector.riotx.core.epoxy
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.util.Cancelable
import io.reactivex.SingleEmitter
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
internal class MatrixCallbackSingle<T>(private val singleEmitter: SingleEmitter<T>) : MatrixCallback<T> {
/**
* Item of size (0, 0).
* It can be useful to avoid automatic scroll of RecyclerView with Epoxy controller, when the first valuable item changes.
*/
@EpoxyModelClass(layout = R.layout.item_zero)
abstract class ZeroItem : VectorEpoxyModel<ZeroItem.Holder>() {
override fun onSuccess(data: T) {
singleEmitter.onSuccess(data)
}
override fun onFailure(failure: Throwable) {
singleEmitter.tryOnError(failure)
}
}
fun <T> Cancelable.toSingle(singleEmitter: SingleEmitter<T>) {
singleEmitter.setCancellable {
this.cancel()
}
class Holder : VectorEpoxyHolder()
}

View File

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

View File

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

View File

@ -25,14 +25,15 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import javax.inject.Inject
class ErrorFormatter @Inject constructor(private val stringProvider: StringProvider) {
interface ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String
}
fun toHumanReadable(failure: Failure): String {
// Default
return failure.localizedMessage
}
class DefaultErrorFormatter @Inject constructor(
private val stringProvider: StringProvider
) : ErrorFormatter {
fun toHumanReadable(throwable: Throwable?): String {
override fun toHumanReadable(throwable: Throwable?): String {
return when (throwable) {
null -> null
is Failure.NetworkConnection -> {
@ -41,6 +42,7 @@ class ErrorFormatter @Inject constructor(private val stringProvider: StringProvi
stringProvider.getString(R.string.error_network_timeout)
throwable.ioException is UnknownHostException ->
// Invalid homeserver?
// TODO Check network state, airplane mode, etc.
stringProvider.getString(R.string.login_error_unknown_host)
else ->
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
stringProvider.getString(R.string.error_terms_not_accepted)
}
throwable.error.code == MatrixError.FORBIDDEN
throwable.error.code == MatrixError.M_FORBIDDEN
&& throwable.error.message == "Invalid password" -> {
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)
}
throwable.error.code == MatrixError.BAD_JSON -> {
throwable.error.code == MatrixError.M_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)
}
throwable.error.code == MatrixError.LIMIT_EXCEEDED -> {
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
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)
}
else -> {

View File

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

View File

@ -0,0 +1,31 @@
/*
* 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.core.error
import im.vector.riotx.BuildConfig
import timber.log.Timber
/**
* throw in debug, only log in production. As this method does not always throw, next statement should be a return
*/
fun fatalError(message: String) {
if (BuildConfig.DEBUG) {
error(message)
} else {
Timber.e(message)
}
}

View File

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

View File

@ -35,3 +35,12 @@ fun StringBuilder.appendParamToUrl(param: String, value: String): StringBuilder
return this
}
/**
* 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.bumptech.glide.util.Util
import com.google.android.material.snackbar.Snackbar
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.di.*
import im.vector.riotx.core.dialogs.DialogLocker
import im.vector.riotx.core.extensions.observeEvent
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.consent.ConsentNotGivenHelper
import im.vector.riotx.features.navigation.Navigator
@ -89,6 +92,9 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
protected lateinit var navigator: Navigator
private lateinit var activeSessionHolder: ActiveSessionHolder
// Filter for multiple invalid token error
private var mainActivityStarted = false
private var unBinder: Unbinder? = null
private var savedInstanceState: Bundle? = null
@ -153,9 +159,8 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
})
sessionListener = getVectorComponent().sessionListener()
sessionListener.consentNotGivenLiveData.observeEvent(this) {
consentNotGivenHelper.displayDialog(it.consentUri,
activeSessionHolder.getActiveSession().sessionParams.homeServerConnectionConfig.homeServerUri.host ?: "")
sessionListener.globalErrorLiveData.observeEvent(this) {
handleGlobalError(it)
}
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() {
super.onDestroy()
unBinder?.unbind()
@ -190,8 +222,7 @@ abstract class VectorBaseActivity : AppCompatActivity(), HasScreenInjector {
override fun onResume() {
super.onResume()
Timber.v("onResume Activity ${this.javaClass.simpleName}")
Timber.i("onResume Activity ${this.javaClass.simpleName}")
configurationViewModel.onActivityResumed()

View File

@ -32,6 +32,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import im.vector.riotx.core.di.DaggerScreenComponent
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.utils.DimensionConverter
import timber.log.Timber
/**
* Add MvRx capabilities to bottomsheetdialog (like BaseMvRxFragment)
@ -80,6 +81,11 @@ abstract class VectorBaseBottomSheetDialogFragment : BottomSheetDialogFragment()
super.onCreate(savedInstanceState)
}
override fun onResume() {
super.onResume()
Timber.i("onResume BottomSheet ${this.javaClass.simpleName}")
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
val dialog = this as? BottomSheetDialog

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.HasScreenInjector
import im.vector.riotx.core.di.ScreenComponent
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.features.navigation.Navigator
import io.reactivex.disposables.CompositeDisposable
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
protected lateinit var navigator: Navigator
protected lateinit var errorFormatter: ErrorFormatter
/* ==========================================================================================
* View model
* ========================================================================================== */
@ -74,6 +77,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
override fun onAttach(context: Context) {
screenComponent = DaggerScreenComponent.factory().create(vectorBaseActivity.getVectorComponent(), vectorBaseActivity)
navigator = screenComponent.navigator()
errorFormatter = screenComponent.errorFormatter()
viewModelFactory = screenComponent.viewModelFactory()
childFragmentManager.fragmentFactory = screenComponent.fragmentFactory()
injectWith(injector())
@ -100,7 +104,7 @@ abstract class VectorBaseFragment : BaseMvRxFragment(), HasScreenInjector {
@CallSuper
override fun onResume() {
super.onResume()
Timber.v("onResume Fragment ${this.javaClass.simpleName}")
Timber.i("onResume Fragment ${this.javaClass.simpleName}")
}
@CallSuper

View File

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

View File

@ -14,9 +14,7 @@
* limitations under the License.
*/
package im.vector.matrix.android.api.failure
package im.vector.riotx.core.ui.model
// This data class will be sent to the bus
data class ConsentNotGivenError(
val consentUri: String
)
// android.util.Size in API 21+
data class Size(val width: Int, val height: Int)

View File

@ -26,6 +26,7 @@ import im.vector.riotx.R
import im.vector.riotx.core.glide.GlideApp
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.toMatrixItem
import kotlinx.android.synthetic.main.view_read_receipts.view.*
private const val MAX_RECEIPT_DISPLAYED = 5
@ -59,7 +60,7 @@ class ReadReceiptsView @JvmOverloads constructor(
receiptAvatars[index].visibility = View.INVISIBLE
} else {
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.content.Intent
import android.os.Bundle
import android.os.Parcelable
import androidx.appcompat.app.AlertDialog
import com.bumptech.glide.Glide
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.failure.GlobalError
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.di.ScreenComponent
@ -30,6 +32,10 @@ import im.vector.riotx.core.platform.VectorBaseActivity
import im.vector.riotx.core.utils.deleteAllFiles
import im.vector.riotx.features.home.HomeActivity
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.GlobalScope
import kotlinx.coroutines.launch
@ -37,23 +43,37 @@ import kotlinx.coroutines.withContext
import timber.log.Timber
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() {
companion object {
private const val EXTRA_CLEAR_CACHE = "EXTRA_CLEAR_CACHE"
private const val EXTRA_CLEAR_CREDENTIALS = "EXTRA_CLEAR_CREDENTIALS"
private const val EXTRA_ARGS = "EXTRA_ARGS"
// 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)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.putExtra(EXTRA_CLEAR_CACHE, clearCache)
intent.putExtra(EXTRA_CLEAR_CREDENTIALS, clearCredentials)
intent.putExtra(EXTRA_ARGS, args)
activity.startActivity(intent)
}
}
private lateinit var args: MainActivityArgs
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
@Inject lateinit var sessionHolder: ActiveSessionHolder
@Inject lateinit var errorFormatter: ErrorFormatter
@ -63,42 +83,71 @@ class MainActivity : VectorBaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val clearCache = intent.getBooleanExtra(EXTRA_CLEAR_CACHE, false)
val clearCredentials = intent.getBooleanExtra(EXTRA_CLEAR_CREDENTIALS, false)
args = parseArgs()
if (args.clearCredentials || args.isUserLoggedOut) {
clearNotifications()
}
// Handle some wanted cleanup
if (clearCache || clearCredentials) {
doCleanUp(clearCache, clearCredentials)
if (args.clearCache || args.clearCredentials) {
doCleanUp()
} else {
start()
startNextActivityAndFinish()
}
}
private fun doCleanUp(clearCache: Boolean, clearCredentials: Boolean) {
private fun clearNotifications() {
// Dismiss all notifications
notificationDrawerManager.clearAllEvents()
notificationDrawerManager.persistInfo()
}
private fun parseArgs(): MainActivityArgs {
val argsFromIntent: MainActivityArgs? = intent.getParcelableExtra(EXTRA_ARGS)
Timber.w("Starting MainActivity with $argsFromIntent")
return MainActivityArgs(
clearCache = argsFromIntent?.clearCache ?: false,
clearCredentials = argsFromIntent?.clearCredentials ?: false,
isUserLoggedOut = argsFromIntent?.isUserLoggedOut ?: false,
isSoftLogout = argsFromIntent?.isSoftLogout ?: false
)
}
private fun doCleanUp() {
when {
clearCredentials -> sessionHolder.getActiveSession().signOut(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
args.clearCredentials -> sessionHolder.getActiveSession().signOut(
!args.isUserLoggedOut,
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
Timber.w("SIGN_OUT: success, start app")
sessionHolder.clearActiveSession()
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
clearCache -> sessionHolder.getActiveSession().clearCache(object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
args.clearCache -> sessionHolder.getActiveSession().clearCache(
object : MatrixCallback<Unit> {
override fun onSuccess(data: Unit) {
doLocalCleanupAndStart()
}
override fun onFailure(failure: Throwable) {
displayError(failure, clearCache, clearCredentials)
}
})
override fun onFailure(failure: Throwable) {
displayError(failure)
}
})
}
}
override fun handleInvalidToken(globalError: GlobalError.InvalidToken) {
// No op here
Timber.w("Ignoring invalid token global error")
}
private fun doLocalCleanupAndStart() {
GlobalScope.launch(Dispatchers.Main) {
// On UI Thread
@ -112,24 +161,43 @@ class MainActivity : VectorBaseActivity() {
}
}
start()
startNextActivityAndFinish()
}
private fun displayError(failure: Throwable, clearCache: Boolean, clearCredentials: Boolean) {
private fun displayError(failure: Throwable) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(failure))
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp(clearCache, clearCredentials) }
.setNegativeButton(R.string.cancel) { _, _ -> start() }
.setPositiveButton(R.string.global_retry) { _, _ -> doCleanUp() }
.setNegativeButton(R.string.cancel) { _, _ -> startNextActivityAndFinish() }
.setCancelable(false)
.show()
}
private fun start() {
val intent = if (sessionHolder.hasActiveSession()) {
HomeActivity.newIntent(this)
} else {
LoginActivity.newIntent(this, null)
private fun startNextActivityAndFinish() {
val intent = when {
args.clearCredentials
&& !args.isUserLoggedOut ->
// 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)
finish()

View File

@ -18,6 +18,7 @@ package im.vector.riotx.features.attachments
import com.kbeanie.multipicker.api.entity.*
import im.vector.matrix.android.api.session.content.ContentAttachmentData
import timber.log.Timber
fun ChosenContact.toContactAttachment(): ContactAttachment {
return ContactAttachment(
@ -29,6 +30,7 @@ fun ChosenContact.toContactAttachment(): ContactAttachment {
}
fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -40,6 +42,7 @@ fun ChosenFile.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -51,16 +54,17 @@ fun ChosenAudio.toContentAttachmentData(): ContentAttachmentData {
)
}
fun ChosenFile.mapType(): ContentAttachmentData.Type {
private fun ChosenFile.mapType(): ContentAttachmentData.Type {
return when {
mimeType.startsWith("image/") -> ContentAttachmentData.Type.IMAGE
mimeType.startsWith("video/") -> ContentAttachmentData.Type.VIDEO
mimeType.startsWith("audio/") -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
mimeType?.startsWith("image/") == true -> ContentAttachmentData.Type.IMAGE
mimeType?.startsWith("video/") == true -> ContentAttachmentData.Type.VIDEO
mimeType?.startsWith("audio/") == true -> ContentAttachmentData.Type.AUDIO
else -> ContentAttachmentData.Type.FILE
}
}
fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,
@ -75,6 +79,7 @@ fun ChosenImage.toContentAttachmentData(): ContentAttachmentData {
}
fun ChosenVideo.toContentAttachmentData(): ContentAttachmentData {
if (mimeType == null) Timber.w("No mimeType")
return ContentAttachmentData(
path = originalPath,
mimeType = mimeType,

View File

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

View File

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

View File

@ -22,6 +22,8 @@ import androidx.lifecycle.Observer
import butterknife.BindView
import butterknife.OnClick
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.core.platform.VectorBaseFragment
import im.vector.riotx.features.home.AvatarRenderer
@ -57,10 +59,10 @@ class SASVerificationIncomingFragment @Inject constructor(
otherDeviceTextView.text = viewModel.otherDeviceId
viewModel.otherUser?.let {
avatarRenderer.render(it, avatarImageView)
avatarRenderer.render(it.toMatrixItem(), avatarImageView)
} ?: run {
// 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 {

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.Target
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.session.user.model.User
import im.vector.matrix.android.internal.util.firstLetterOfDisplayName
import im.vector.riotx.R
import im.vector.matrix.android.api.util.MatrixItem
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
@ -45,76 +42,42 @@ class AvatarRenderer @Inject constructor(private val activeSessionHolder: Active
companion object {
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
fun render(roomSummary: RoomSummary, imageView: ImageView) {
render(roomSummary.avatarUrl, roomSummary.roomId, roomSummary.displayName, imageView)
}
@UiThread
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))
fun render(matrixItem: MatrixItem, imageView: ImageView) {
render(imageView.context,
GlideApp.with(imageView),
matrixItem,
DrawableImageViewTarget(imageView))
}
@UiThread
fun render(context: Context,
glideRequest: GlideRequests,
avatarUrl: String?,
identifier: String,
name: String?,
matrixItem: MatrixItem,
target: Target<Drawable>) {
val displayName = if (name.isNullOrBlank()) {
identifier
} else {
name
}
val placeholder = getPlaceholderDrawable(context, identifier, displayName)
buildGlideRequest(glideRequest, avatarUrl)
val placeholder = getPlaceholderDrawable(context, matrixItem)
buildGlideRequest(glideRequest, matrixItem.avatarUrl)
.placeholder(placeholder)
.into(target)
}
@AnyThread
fun getPlaceholderDrawable(context: Context, identifier: String, text: String): Drawable {
val avatarColor = ContextCompat.getColor(context, getColorFromUserId(identifier))
return if (text.isEmpty()) {
TextDrawable.builder().buildRound("", avatarColor)
} else {
val firstLetter = text.firstLetterOfDisplayName()
TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(firstLetter, avatarColor)
fun getPlaceholderDrawable(context: Context, matrixItem: MatrixItem): Drawable {
val avatarColor = when (matrixItem) {
is MatrixItem.UserItem -> ContextCompat.getColor(context, getColorFromUserId(matrixItem.id))
else -> ContextCompat.getColor(context, getColorFromRoomId(matrixItem.id))
}
return TextDrawable.builder()
.beginConfig()
.bold()
.endConfig()
.buildRound(matrixItem.firstLetterOfDisplayName(), avatarColor)
}
// 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> {
val resolvedUrl = activeSessionHolder.getActiveSession().contentUrlResolver()
.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 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.util.toMatrixItem
import im.vector.riotx.R
import im.vector.riotx.core.extensions.commitTransactionNow
import im.vector.riotx.core.platform.ToolbarConfigurable
@ -74,12 +75,7 @@ class HomeDetailFragment @Inject constructor(
private fun onGroupChange(groupSummary: GroupSummary?) {
groupSummary?.let {
avatarRenderer.render(
it.avatarUrl,
it.groupId,
it.displayName,
groupToolbarAvatarImageView
)
avatarRenderer.render(it.toMatrixItem(), groupToolbarAvatarImageView)
}
}
@ -155,7 +151,7 @@ class HomeDetailFragment @Inject constructor(
bottomNavigationView.selectedItemId = when (displayMode) {
RoomListDisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListDisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home
else -> R.id.bottom_action_home
}
}

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