Merge branch 'develop' into feature/read_marker

This commit is contained in:
ganfra 2019-09-26 12:19:40 +02:00
commit a3f561d788
59 changed files with 945 additions and 321 deletions

View file

@ -5,13 +5,14 @@ Features:
-
Improvements:
-
- Persist active tab between sessions (#503)
- Do not upload file too big for the homeserver (#587)
Other changes:
-
Bugfix:
-
- Fix issue on upload error in loop (#587)
Translations:
-
@ -19,6 +20,12 @@ Translations:
Build:
-
Changes in RiotX 0.6.1 (2019-09-24)
===================================================
Bugfix:
- Fix crash: MergedHeaderItem was missing dimensionConverter
Changes in RiotX 0.6.0 (2019-09-24)
===================================================

View file

@ -22,6 +22,7 @@ import okhttp3.Interceptor
import okhttp3.Response
import okhttp3.logging.HttpLoggingInterceptor
import okio.Buffer
import timber.log.Timber
import java.io.IOException
import java.nio.charset.Charset
import javax.inject.Inject
@ -58,15 +59,21 @@ internal class CurlLoggingInterceptor @Inject constructor(private val logger: Ht
val requestBody = request.body()
if (requestBody != null) {
val buffer = Buffer()
requestBody.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
if (requestBody.contentLength() > 100_000) {
Timber.w("Unable to log curl command data, size is too big (${requestBody.contentLength()})")
// Ensure the curl command will failed
curlCmd += "DATA IS TOO BIG"
} else {
val buffer = Buffer()
requestBody.writeTo(buffer)
var charset: Charset? = UTF8
val contentType = requestBody.contentType()
if (contentType != null) {
charset = contentType.charset(UTF8)
}
// try to keep to a single line and use a subshell to preserve any line breaks
curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'"
}
// try to keep to a single line and use a subshell to preserve any line breaks
curlCmd += " --data $'" + buffer.readString(charset!!).replace("\n", "\\n") + "'"
}
val headers = request.headers()

View file

@ -26,6 +26,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
@ -52,6 +53,7 @@ interface Session :
PushRuleService,
PushersService,
InitialSyncProgressService,
HomeServerCapabilitiesService,
SecureStorageService {
/**

View file

@ -0,0 +1,28 @@
/*
* 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.session.homeserver
data class HomeServerCapabilities(
/**
* Max size of file which can be uploaded to the homeserver in bytes. [MAX_UPLOAD_FILE_SIZE_UNKNOWN] if unknown or not retrieved yet
*/
val maxUploadFileSize: Long
) {
companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.api.session.homeserver
/**
* This interface defines a method to retrieve the homeserver capabilities.
*/
interface HomeServerCapabilitiesService {
/**
* Get the HomeServer capabilities
*/
fun getHomeServerCapabilities(): HomeServerCapabilities
}

View file

@ -117,13 +117,13 @@ internal abstract class CryptoModule {
abstract fun bindGetDevicesTask(getDevicesTask: DefaultGetDevicesTask): GetDevicesTask
@Binds
abstract fun bindSetDeviceNameTask(getDevicesTask: DefaultSetDeviceNameTask): SetDeviceNameTask
abstract fun bindSetDeviceNameTask(setDeviceNameTask: DefaultSetDeviceNameTask): SetDeviceNameTask
@Binds
abstract fun bindUploadKeysTask(getDevicesTask: DefaultUploadKeysTask): UploadKeysTask
abstract fun bindUploadKeysTask(uploadKeysTask: DefaultUploadKeysTask): UploadKeysTask
@Binds
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsers: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
abstract fun bindDownloadKeysForUsersTask(downloadKeysForUsersTask: DefaultDownloadKeysForUsers): DownloadKeysForUsersTask
@Binds
abstract fun bindCreateKeysBackupVersionTask(createKeysBackupVersionTask: DefaultCreateKeysBackupVersionTask): CreateKeysBackupVersionTask
@ -135,10 +135,10 @@ internal abstract class CryptoModule {
abstract fun bindDeleteRoomSessionDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionDataTask): DeleteRoomSessionDataTask
@Binds
abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
abstract fun bindDeleteRoomSessionsDataTask(deleteRoomSessionsDataTask: DefaultDeleteRoomSessionsDataTask): DeleteRoomSessionsDataTask
@Binds
abstract fun bindDeleteSessionsDataTask(deleteRoomSessionDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
abstract fun bindDeleteSessionsDataTask(deleteSessionsDataTask: DefaultDeleteSessionsDataTask): DeleteSessionsDataTask
@Binds
abstract fun bindGetKeysBackupLastVersionTask(getKeysBackupLastVersionTask: DefaultGetKeysBackupLastVersionTask): GetKeysBackupLastVersionTask
@ -150,19 +150,19 @@ internal abstract class CryptoModule {
abstract fun bindGetRoomSessionDataTask(getRoomSessionDataTask: DefaultGetRoomSessionDataTask): GetRoomSessionDataTask
@Binds
abstract fun bindGetRoomSessionsDataTask(getRoomSessionDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
abstract fun bindGetRoomSessionsDataTask(getRoomSessionsDataTask: DefaultGetRoomSessionsDataTask): GetRoomSessionsDataTask
@Binds
abstract fun bindGetSessionsDataTask(getRoomSessionDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask
abstract fun bindGetSessionsDataTask(getSessionsDataTask: DefaultGetSessionsDataTask): GetSessionsDataTask
@Binds
abstract fun bindStoreRoomSessionDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionDataTask): StoreRoomSessionDataTask
@Binds
abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
abstract fun bindStoreRoomSessionsDataTask(storeRoomSessionsDataTask: DefaultStoreRoomSessionsDataTask): StoreRoomSessionsDataTask
@Binds
abstract fun bindStoreSessionsDataTask(storeRoomSessionDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask
abstract fun bindStoreSessionsDataTask(storeSessionsDataTask: DefaultStoreSessionsDataTask): StoreSessionsDataTask
@Binds
abstract fun bindUpdateKeysBackupVersionTask(updateKeysBackupVersionTask: DefaultUpdateKeysBackupVersionTask): UpdateKeysBackupVersionTask

View file

@ -17,7 +17,6 @@
package im.vector.matrix.android.internal.crypto.attachments
import android.util.Base64
import arrow.core.Try
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileInfo
import im.vector.matrix.android.internal.crypto.model.rest.EncryptedFileKey
import timber.log.Timber
@ -50,7 +49,7 @@ object MXEncryptedAttachments {
* @param mimetype the mime type
* @return the encryption file info
*/
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): Try<EncryptionResult> {
fun encryptAttachment(attachmentStream: InputStream, mimetype: String): EncryptionResult {
val t0 = System.currentTimeMillis()
val secureRandom = SecureRandom()
@ -70,7 +69,7 @@ object MXEncryptedAttachments {
val outStream = ByteArrayOutputStream()
try {
outStream.use {
val encryptCipher = Cipher.getInstance(CIPHER_ALGORITHM)
val secretKeySpec = SecretKeySpec(key, SECRET_KEY_SPEC_ALGORITHM)
val ivParameterSpec = IvParameterSpec(initVectorBytes)
@ -114,19 +113,7 @@ object MXEncryptedAttachments {
)
Timber.v("Encrypt in ${System.currentTimeMillis() - t0} ms")
return Try.just(result)
} catch (oom: OutOfMemoryError) {
Timber.e(oom, "## encryptAttachment failed")
return Try.Failure(oom)
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment failed")
return Try.Failure(e)
} finally {
try {
outStream.close()
} catch (e: Exception) {
Timber.e(e, "## encryptAttachment() : fail to close outStream")
}
return result
}
}

View file

@ -0,0 +1,38 @@
/*
* 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.database.mapper
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
/**
* HomeServerCapabilitiesEntity <-> HomeSeverCapabilities
*/
internal object HomeServerCapabilitiesMapper {
fun map(entity: HomeServerCapabilitiesEntity): HomeServerCapabilities {
return HomeServerCapabilities(
entity.maxUploadFileSize
)
}
fun map(domain: HomeServerCapabilities): HomeServerCapabilitiesEntity {
return HomeServerCapabilitiesEntity(
domain.maxUploadFileSize
)
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.database.model
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import io.realm.RealmObject
internal open class HomeServerCapabilitiesEntity(
var maxUploadFileSize: Long = HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN,
var lastUpdatedTimestamp: Long = 0L
) : RealmObject() {
companion object
}

View file

@ -46,6 +46,7 @@ import io.realm.annotations.RealmModule
ReadReceiptsSummaryEntity::class,
ReadMarkerEntity::class,
UserDraftsEntity::class,
DraftEntity::class
DraftEntity::class,
HomeServerCapabilitiesEntity::class
])
internal class SessionRealmModule

View file

@ -0,0 +1,37 @@
/*
* 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.database.query
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import io.realm.Realm
import io.realm.kotlin.createObject
import io.realm.kotlin.where
/**
* Get the current HomeServerCapabilitiesEntity, create one if it does not exist
*/
internal fun HomeServerCapabilitiesEntity.Companion.getOrCreate(realm: Realm): HomeServerCapabilitiesEntity {
var homeServerCapabilitiesEntity = realm.where<HomeServerCapabilitiesEntity>().findFirst()
if (homeServerCapabilitiesEntity == null) {
realm.executeTransaction {
realm.createObject<HomeServerCapabilitiesEntity>()
}
homeServerCapabilitiesEntity = realm.where<HomeServerCapabilitiesEntity>().findFirst()!!
}
return homeServerCapabilitiesEntity
}

View file

@ -22,4 +22,9 @@ internal object NetworkConstants {
const val URI_API_PREFIX_PATH_R0 = "$URI_API_PREFIX_PATH/r0/"
const val URI_API_PREFIX_PATH_UNSTABLE = "$URI_API_PREFIX_PATH/unstable/"
// Media
private const val URI_API_MEDIA_PREFIX_PATH = "_matrix/media"
const val URI_API_MEDIA_PREFIX_PATH_R0 = "$URI_API_MEDIA_PREFIX_PATH/r0/"
}

View file

@ -16,24 +16,15 @@
package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.CancellationException
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import timber.log.Timber
import java.io.IOException
internal suspend inline fun <DATA> executeRequest(block: Request<DATA>.() -> Unit) = Request<DATA>().apply(block).execute()
internal class Request<DATA> {
private val moshi: Moshi = MoshiProvider.providesMoshi()
lateinit var apiCall: Call<DATA>
suspend fun execute(): DATA {
@ -43,7 +34,7 @@ internal class Request<DATA> {
response.body()
?: throw IllegalStateException("The request returned a null body")
} else {
throw manageFailure(response.errorBody(), response.code())
throw response.toFailure()
}
} catch (exception: Throwable) {
throw when (exception) {
@ -55,32 +46,4 @@ internal class Request<DATA> {
}
}
}
private fun manageFailure(errorBody: ResponseBody?, httpCode: Int): Throwable {
if (errorBody == null) {
return RuntimeException("Error body should not be null")
}
val errorBodyStr = errorBody.string()
val matrixErrorAdapter = moshi.adapter(MatrixError::class.java)
try {
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
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))
}
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {
// This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError")
}
return Failure.OtherServerError(errorBodyStr, httpCode)
}
}

View file

@ -18,14 +18,23 @@
package im.vector.matrix.android.internal.network
import com.squareup.moshi.JsonDataException
import im.vector.matrix.android.api.failure.ConsentNotGivenError
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
import im.vector.matrix.android.internal.di.MoshiProvider
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ResponseBody
import org.greenrobot.eventbus.EventBus
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import timber.log.Timber
import java.io.IOException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
internal suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
@ -40,4 +49,64 @@ suspend fun <T> Call<T>.awaitResponse(): Response<T> {
}
})
}
}
}
internal suspend fun okhttp3.Call.awaitResponse(): okhttp3.Response {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : okhttp3.Callback {
override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
continuation.resume(response)
}
override fun onFailure(call: okhttp3.Call, e: IOException) {
continuation.resumeWithException(e)
}
})
}
}
/**
* Convert a retrofit Response to a Failure, and eventually parse errorBody to convert it to a MatrixError
*/
internal fun <T> Response<T>.toFailure(): Failure {
return toFailure(errorBody(), code())
}
/**
* Convert a okhttp3 Response to a Failure, and eventually parse errorBody to convert it to a MatrixError
*/
internal fun okhttp3.Response.toFailure(): Failure {
return toFailure(body(), code())
}
private fun toFailure(errorBody: ResponseBody?, httpCode: Int): Failure {
if (errorBody == null) {
return Failure.Unknown(RuntimeException("errorBody should not be null"))
}
val errorBodyStr = errorBody.string()
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
try {
val matrixError = matrixErrorAdapter.fromJson(errorBodyStr)
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))
}
return Failure.ServerError(matrixError, httpCode)
}
} catch (ex: JsonDataException) {
// This is not a MatrixError
Timber.w("The error returned by the server is not a MatrixError")
}
return Failure.OtherServerError(errorBodyStr, httpCode)
}

View file

@ -32,6 +32,7 @@ import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.crypto.CryptoService
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.pushers.PushersService
import im.vector.matrix.android.api.session.room.RoomDirectoryService
import im.vector.matrix.android.api.session.room.RoomService
@ -68,7 +69,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
private val syncThreadProvider: Provider<SyncThread>,
private val contentUrlResolver: ContentUrlResolver,
private val contentUploadProgressTracker: ContentUploadStateTracker,
private val initialSyncProgressService: Lazy<InitialSyncProgressService>)
private val initialSyncProgressService: Lazy<InitialSyncProgressService>,
private val homeServerCapabilitiesService: Lazy<HomeServerCapabilitiesService>)
: Session,
RoomService by roomService.get(),
RoomDirectoryService by roomDirectoryService.get(),
@ -81,7 +83,8 @@ internal class DefaultSession @Inject constructor(override val sessionParams: Se
PushersService by pushersService.get(),
FileService by fileService.get(),
InitialSyncProgressService by initialSyncProgressService.get(),
SecureStorageService by secureStorageService.get() {
SecureStorageService by secureStorageService.get(),
HomeServerCapabilitiesService by homeServerCapabilitiesService.get() {
private var isOpen = false

View file

@ -30,6 +30,7 @@ import im.vector.matrix.android.internal.session.content.UploadContentWorker
import im.vector.matrix.android.internal.session.filter.FilterModule
import im.vector.matrix.android.internal.session.group.GetGroupDataWorker
import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.homeserver.HomeServerCapabilitiesModule
import im.vector.matrix.android.internal.session.pushers.AddHttpPusherWorker
import im.vector.matrix.android.internal.session.pushers.PushersModule
import im.vector.matrix.android.internal.session.room.RoomModule
@ -51,6 +52,7 @@ import im.vector.matrix.android.internal.task.TaskExecutor
SessionModule::class,
RoomModule::class,
SyncModule::class,
HomeServerCapabilitiesModule::class,
SignOutModule::class,
GroupModule::class,
UserModule::class,

View file

@ -27,6 +27,7 @@ import im.vector.matrix.android.api.auth.data.HomeServerConnectionConfig
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.InitialSyncProgressService
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.api.session.securestorage.SecureStorageService
import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.database.RealmKeysUtils
@ -36,6 +37,7 @@ import im.vector.matrix.android.internal.network.AccessTokenInterceptor
import im.vector.matrix.android.internal.network.RetrofitFactory
import im.vector.matrix.android.internal.network.interceptors.CurlLoggingInterceptor
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.homeserver.DefaultHomeServerCapabilitiesService
import im.vector.matrix.android.internal.session.room.EventRelationsAggregationUpdater
import im.vector.matrix.android.internal.session.room.create.RoomCreateEventLiveObserver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner
@ -162,7 +164,7 @@ internal abstract class SessionModule {
@Binds
@IntoSet
abstract fun bindEventRelationsAggregationUpdater(groupSummaryUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
abstract fun bindEventRelationsAggregationUpdater(eventRelationsAggregationUpdater: EventRelationsAggregationUpdater): LiveEntityObserver
@Binds
@IntoSet
@ -178,4 +180,7 @@ internal abstract class SessionModule {
@Binds
abstract fun bindSecureStorageService(secureStorageService: DefaultSecureStorageService): SecureStorageService
@Binds
abstract fun bindHomeServerCapabilitiesService(homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService): HomeServerCapabilitiesService
}

View file

@ -16,12 +16,12 @@
package im.vector.matrix.android.internal.session.content
import arrow.core.Try
import arrow.core.Try.Companion.raise
import com.squareup.moshi.Moshi
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.internal.di.Authenticated
import im.vector.matrix.android.internal.network.ProgressRequestBody
import im.vector.matrix.android.internal.network.awaitResponse
import im.vector.matrix.android.internal.network.toFailure
import okhttp3.*
import java.io.File
import java.io.IOException
@ -37,28 +37,26 @@ internal class FileUploader @Inject constructor(@Authenticated
private val responseAdapter = moshi.adapter(ContentUploadResponse::class.java)
fun uploadFile(file: File,
filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {
suspend fun uploadFile(file: File,
filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = RequestBody.create(MediaType.parse(mimeType), file)
return upload(uploadBody, filename, progressListener)
}
fun uploadByteArray(byteArray: ByteArray,
filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): Try<ContentUploadResponse> {
suspend fun uploadByteArray(byteArray: ByteArray,
filename: String?,
mimeType: String,
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
val uploadBody = RequestBody.create(MediaType.parse(mimeType), byteArray)
return upload(uploadBody, filename, progressListener)
}
private fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): Try<ContentUploadResponse> {
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: return raise(RuntimeException())
private suspend fun upload(uploadBody: RequestBody, filename: String?, progressListener: ProgressRequestBody.Listener?): ContentUploadResponse {
val urlBuilder = HttpUrl.parse(uploadUrl)?.newBuilder() ?: throw RuntimeException()
val httpUrl = urlBuilder
.addQueryParameter("filename", filename)
@ -71,19 +69,15 @@ internal class FileUploader @Inject constructor(@Authenticated
.post(requestBody)
.build()
return Try {
okHttpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) {
throw IOException()
} else {
response.body()?.source()?.let {
responseAdapter.fromJson(it)
}
?: throw IOException()
return okHttpClient.newCall(request).awaitResponse().use { response ->
if (!response.isSuccessful) {
throw response.toFailure()
} else {
response.body()?.source()?.let {
responseAdapter.fromJson(it)
}
?: throw IOException()
}
}
}
}

View file

@ -93,32 +93,28 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
}
}
val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt thumbnail")
contentUploadStateTracker.setEncryptingThumbnail(eventId)
MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
.flatMap { encryptionResult ->
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
try {
val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt thumbnail")
contentUploadStateTracker.setEncryptingThumbnail(eventId)
val encryptionResult = MXEncryptedAttachments.encryptAttachment(ByteArrayInputStream(thumbnailData.bytes), thumbnailData.mimeType)
uploadedThumbnailEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
} else {
fileUploader.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
}
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray,
"thumb_${attachment.name}",
"application/octet-stream",
thumbnailProgressListener)
}
} else {
fileUploader
.uploadByteArray(thumbnailData.bytes,
"thumb_${attachment.name}",
thumbnailData.mimeType,
thumbnailProgressListener)
uploadedThumbnailUrl = contentUploadResponse.contentUri
} catch (t: Throwable) {
Timber.e(t)
return handleFailure(params, t)
}
contentUploadResponse
.fold(
{ Timber.e(it) },
{ uploadedThumbnailUrl = it.contentUri }
)
}
val progressListener = object : ProgressRequestBody.Listener {
@ -133,27 +129,26 @@ internal class UploadContentWorker(context: Context, params: WorkerParameters) :
var uploadedFileEncryptedFileInfo: EncryptedFileInfo? = null
val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt file")
contentUploadStateTracker.setEncrypting(eventId)
return try {
val contentUploadResponse = if (params.isRoomEncrypted) {
Timber.v("Encrypt file")
contentUploadStateTracker.setEncrypting(eventId)
MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
.flatMap { encryptionResult ->
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
val encryptionResult = MXEncryptedAttachments.encryptAttachment(FileInputStream(attachmentFile), attachment.mimeType)
uploadedFileEncryptedFileInfo = encryptionResult.encryptedFileInfo
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
}
} else {
fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
fileUploader
.uploadByteArray(encryptionResult.encryptedByteArray, attachment.name, "application/octet-stream", progressListener)
} else {
fileUploader
.uploadFile(attachmentFile, attachment.name, attachment.mimeType, progressListener)
}
handleSuccess(params, contentUploadResponse.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo)
} catch (t: Throwable) {
Timber.e(t)
handleFailure(params, t)
}
return contentUploadResponse
.fold(
{ handleFailure(params, it) },
{ handleSuccess(params, it.contentUri, uploadedFileEncryptedFileInfo, uploadedThumbnailUrl, uploadedThumbnailEncryptedFileInfo) }
)
}
private fun handleFailure(params: Params, failure: Throwable): Result {

View file

@ -43,7 +43,7 @@ internal abstract class FilterModule {
abstract fun bindFilterService(filterService: DefaultFilterService): FilterService
@Binds
abstract fun bindSaveFilterTask(saveFilterTask_Factory: DefaultSaveFilterTask): SaveFilterTask
abstract fun bindSaveFilterTask(saveFilterTask: DefaultSaveFilterTask): SaveFilterTask
}

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.matrix.android.internal.session.homeserver
import im.vector.matrix.android.internal.network.NetworkConstants
import retrofit2.Call
import retrofit2.http.GET
internal interface CapabilitiesAPI {
/**
* Request the upload capabilities
*/
@GET(NetworkConstants.URI_API_MEDIA_PREFIX_PATH_R0 + "config")
fun getUploadCapabilities(): Call<GetUploadCapabilitiesResult>
}

View file

@ -0,0 +1,75 @@
/*
* 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.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.awaitTransaction
import java.util.*
import javax.inject.Inject
internal interface GetHomeServerCapabilitiesTask : Task<Unit, Unit>
internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
private val capabilitiesAPI: CapabilitiesAPI,
private val monarchy: Monarchy
) : GetHomeServerCapabilitiesTask {
override suspend fun execute(params: Unit) {
var doRequest = false
monarchy.awaitTransaction { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
doRequest = homeServerCapabilitiesEntity.lastUpdatedTimestamp + MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS < Date().time
}
if (!doRequest) {
return
}
val uploadCapabilities = executeRequest<GetUploadCapabilitiesResult> {
apiCall = capabilitiesAPI.getUploadCapabilities()
}
// TODO Add other call here (get version, etc.)
insertInDb(uploadCapabilities)
}
private fun insertInDb(getUploadCapabilitiesResult: GetUploadCapabilitiesResult) {
monarchy
.writeAsync { realm ->
val homeServerCapabilitiesEntity = HomeServerCapabilitiesEntity.getOrCreate(realm)
homeServerCapabilitiesEntity.maxUploadFileSize = getUploadCapabilitiesResult.maxUploadSize
?: HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
homeServerCapabilitiesEntity.lastUpdatedTimestamp = Date().time
}
}
companion object {
// 8 hours like on Riot Web
private const val MIN_DELAY_BETWEEN_TWO_REQUEST_MILLIS = 8 * 60 * 60 * 1000
}
}

View file

@ -0,0 +1,44 @@
/*
* 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.homeserver
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilitiesService
import im.vector.matrix.android.internal.database.mapper.HomeServerCapabilitiesMapper
import im.vector.matrix.android.internal.database.model.HomeServerCapabilitiesEntity
import im.vector.matrix.android.internal.database.query.getOrCreate
import javax.inject.Inject
internal class DefaultHomeServerCapabilitiesService @Inject constructor(private val monarchy: Monarchy) : HomeServerCapabilitiesService {
override fun getHomeServerCapabilities(): HomeServerCapabilities {
var entity: HomeServerCapabilitiesEntity? = null
monarchy.doWithRealm { realm ->
entity = HomeServerCapabilitiesEntity.getOrCreate(realm)
}
return with(entity) {
if (this != null) {
HomeServerCapabilitiesMapper.map(this)
} else {
// Should not happen
HomeServerCapabilities(HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN)
}
}
}
}

View file

@ -0,0 +1,30 @@
/*
* 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.homeserver
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class GetUploadCapabilitiesResult(
/**
* The maximum size an upload can be in bytes. Clients SHOULD use this as a guide when uploading content.
* If not listed or null, the size limit should be treated as unknown.
*/
@Json(name = "m.upload.size")
val maxUploadSize: Long? = null
)

View file

@ -0,0 +1,41 @@
/*
* 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.homeserver
import dagger.Binds
import dagger.Module
import dagger.Provides
import im.vector.matrix.android.internal.session.SessionScope
import retrofit2.Retrofit
@Module
internal abstract class HomeServerCapabilitiesModule {
@Module
companion object {
@Provides
@JvmStatic
@SessionScope
fun providesCapabilitiesAPI(retrofit: Retrofit): CapabilitiesAPI {
return retrofit.create(CapabilitiesAPI::class.java)
}
}
@Binds
abstract fun bindGetHomeServerCapabilitiesTask(getHomeServerCapabilitiesTask: DefaultGetHomeServerCapabilitiesTask): GetHomeServerCapabilitiesTask
}

View file

@ -26,8 +26,8 @@ import javax.inject.Inject
internal interface GetPushersTask : Task<Unit, Unit>
internal class DefaultGetPusherTask @Inject constructor(private val pushersAPI: PushersAPI,
private val monarchy: Monarchy) : GetPushersTask {
internal class DefaultGetPushersTask @Inject constructor(private val pushersAPI: PushersAPI,
private val monarchy: Monarchy) : GetPushersTask {
override suspend fun execute(params: Unit) {
val response = executeRequest<GetPushersResponse> {

View file

@ -54,7 +54,7 @@ internal abstract class PushersModule {
abstract fun bindConditionResolver(conditionResolver: DefaultConditionResolver): ConditionResolver
@Binds
abstract fun bindGetPushersTask(getPusherTask: DefaultGetPusherTask): GetPushersTask
abstract fun bindGetPushersTask(getPushersTask: DefaultGetPushersTask): GetPushersTask
@Binds
abstract fun bindGetPushRulesTask(getPushRulesTask: DefaultGetPushRulesTask): GetPushRulesTask

View file

@ -127,5 +127,5 @@ internal abstract class RoomModule {
abstract fun bindFileService(fileService: DefaultFileService): FileService
@Binds
abstract fun bindFetchEditHistoryTask(editHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
abstract fun bindFetchEditHistoryTask(fetchEditHistoryTask: DefaultFetchEditHistoryTask): FetchEditHistoryTask
}

View file

@ -16,7 +16,6 @@
package im.vector.matrix.android.internal.session.sync
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.R
import im.vector.matrix.android.api.failure.Failure
import im.vector.matrix.android.api.failure.MatrixError
@ -25,6 +24,7 @@ import im.vector.matrix.android.internal.di.UserId
import im.vector.matrix.android.internal.network.executeRequest
import im.vector.matrix.android.internal.session.DefaultInitialSyncProgressService
import im.vector.matrix.android.internal.session.filter.FilterRepository
import im.vector.matrix.android.internal.session.homeserver.GetHomeServerCapabilitiesTask
import im.vector.matrix.android.internal.session.sync.model.SyncResponse
import im.vector.matrix.android.internal.task.Task
import javax.inject.Inject
@ -42,11 +42,14 @@ internal class DefaultSyncTask @Inject constructor(private val syncAPI: SyncAPI,
private val sessionParamsStore: SessionParamsStore,
private val initialSyncProgressService: DefaultInitialSyncProgressService,
private val syncTokenStore: SyncTokenStore,
private val monarchy: Monarchy
private val getHomeServerCapabilitiesTask: GetHomeServerCapabilitiesTask
) : SyncTask {
override suspend fun execute(params: SyncTask.Params) {
// Maybe refresh the home server capabilities data we know
getHomeServerCapabilitiesTask.execute(Unit)
val requestParams = HashMap<String, String>()
var timeout = 0L
val token = syncTokenStore.getLastToken()

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Note to translator: please add here only the string which are different than default version (which is en-rGB) -->
<string name="verification_emoji_wrench">Wrench</string>
<string name="verification_emoji_airplane">Airplane</string>
</resources>

View file

@ -149,4 +149,10 @@ android\.app\.AlertDialog
new Gson\(\)
### Use matrixOneTimeWorkRequestBuilder
import androidx.work.OneTimeWorkRequestBuilder===1
import androidx.work.OneTimeWorkRequestBuilder===1
### Use TextUtils.formatFileSize
Formatter\.formatFileSize===1
### Use TextUtils.formatFileSize with short format param to true
Formatter\.formatShortFileSize===1

View file

@ -32,6 +32,7 @@ cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-da/strings.xml ./mat
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-de/strings.xml ./matrix-sdk-android/src/main/res/values-de/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-el/strings.xml ./matrix-sdk-android/src/main/res/values-el/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eo/strings.xml ./matrix-sdk-android/src/main/res/values-eo/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-en-rUS/strings.xml ./matrix-sdk-android/src/main/res/values-en-rUS/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es/strings.xml ./matrix-sdk-android/src/main/res/values-es/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-es-rMX/strings.xml ./matrix-sdk-android/src/main/res/values-es-rMX/strings.xml
cp ../matrix-android-sdk/matrix-sdk/src/main/res/values-eu/strings.xml ./matrix-sdk-android/src/main/res/values-eu/strings.xml

View file

@ -65,6 +65,7 @@ import im.vector.riotx.features.roomdirectory.picker.RoomDirectoryPickerFragment
import im.vector.riotx.features.roomdirectory.roompreview.RoomPreviewNoPreviewFragment
import im.vector.riotx.features.settings.*
import im.vector.riotx.features.settings.push.PushGatewaysFragment
import im.vector.riotx.features.ui.UiStateRepository
@Component(dependencies = [VectorComponent::class], modules = [AssistedInjectModule::class, ViewModelModule::class, HomeModule::class])
@ScreenScope
@ -80,6 +81,8 @@ interface ScreenComponent {
fun navigator(): Navigator
fun uiStateRepository(): UiStateRepository
fun inject(activity: HomeActivity)
fun inject(roomDetailFragment: RoomDetailFragment)

View file

@ -42,13 +42,14 @@ import im.vector.riotx.features.rageshake.BugReporter
import im.vector.riotx.features.rageshake.VectorFileLogger
import im.vector.riotx.features.rageshake.VectorUncaughtExceptionHandler
import im.vector.riotx.features.settings.VectorPreferences
import im.vector.riotx.features.ui.UiStateRepository
import javax.inject.Singleton
@Component(modules = [VectorModule::class])
@Singleton
interface VectorComponent {
fun inject(vectorApplication: NotificationBroadcastReceiver)
fun inject(notificationBroadcastReceiver: NotificationBroadcastReceiver)
fun inject(vectorApplication: VectorApplication)
@ -64,7 +65,7 @@ interface VectorComponent {
fun resources(): Resources
fun dimensionUtils(): DimensionConverter
fun dimensionConverter(): DimensionConverter
fun vectorConfiguration(): VectorConfiguration
@ -106,6 +107,8 @@ interface VectorComponent {
fun vectorFileLogger(): VectorFileLogger
fun uiStateRepository(): UiStateRepository
@Component.Factory
interface Factory {
fun create(@BindsInstance context: Context): VectorComponent

View file

@ -28,6 +28,8 @@ import im.vector.matrix.android.api.auth.Authenticator
import im.vector.matrix.android.api.session.Session
import im.vector.riotx.features.navigation.DefaultNavigator
import im.vector.riotx.features.navigation.Navigator
import im.vector.riotx.features.ui.SharedPreferencesUiStateRepository
import im.vector.riotx.features.ui.UiStateRepository
@Module
abstract class VectorModule {
@ -62,7 +64,7 @@ abstract class VectorModule {
@Provides
@JvmStatic
fun providesAuthenticator(matrix: Matrix): Authenticator{
fun providesAuthenticator(matrix: Matrix): Authenticator {
return matrix.authenticator()
}
}
@ -70,5 +72,7 @@ abstract class VectorModule {
@Binds
abstract fun bindNavigator(navigator: DefaultNavigator): Navigator
@Binds
abstract fun bindUiStateRepository(uiStateRepository: SharedPreferencesUiStateRepository): UiStateRepository
}

View file

@ -24,6 +24,8 @@ import java.io.File
// Implementation should return true in case of success
typealias ActionOnFile = (file: File) -> Boolean
internal fun String?.isLocalFile() = this != null && File(this).exists()
/* ==========================================================================================
* Delete
* ========================================================================================== */

View file

@ -16,6 +16,9 @@
package im.vector.riotx.core.utils
import android.content.Context
import android.os.Build
import android.text.format.Formatter
import java.util.*
object TextUtils {
@ -42,4 +45,28 @@ object TextUtils {
return value.toString()
}
}
/**
* Since Android O, the system considers that 1ko = 1000 bytes instead of 1024 bytes. We want to avoid that for the moment.
*/
fun formatFileSize(context: Context, sizeBytes: Long, useShortFormat: Boolean = false): String {
val normalizedSize = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
sizeBytes
} else {
// First convert the size
when {
sizeBytes < 1024 -> sizeBytes
sizeBytes < 1024 * 1024 -> sizeBytes * 1000 / 1024
sizeBytes < 1024 * 1024 * 1024 -> sizeBytes * 1000 / 1024 * 1000 / 1024
else -> sizeBytes * 1000 / 1024 * 1000 / 1024 * 1000 / 1024
}
}
return if (useShortFormat) {
Formatter.formatShortFileSize(context, normalizedSize)
} else {
Formatter.formatFileSize(context, normalizedSize)
}
}
}

View file

@ -16,7 +16,6 @@
package im.vector.riotx.features.home
import android.app.ProgressDialog
import android.content.Context
import android.content.Intent
import android.os.Bundle
@ -66,8 +65,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
@Inject lateinit var pushManager: PushersManager
@Inject lateinit var notificationDrawerManager: NotificationDrawerManager
private var progress: ProgressDialog? = null
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
@ -93,18 +90,6 @@ class HomeActivity : VectorBaseActivity(), ToolbarConfigurable {
replaceFragment(homeDrawerFragment, R.id.homeDrawerFragmentContainer)
}
homeActivityViewModel.isLoading.observe(this, Observer<Boolean> {
// TODO better UI
if (it) {
progress?.dismiss()
progress = ProgressDialog(this)
progress?.setMessage(getString(R.string.room_recents_create_room))
progress?.show()
} else {
progress?.dismiss()
}
})
navigationViewModel.navigateTo.observeEvent(this) { navigation ->
when (navigation) {
is Navigation.OpenDrawer -> drawerLayout.openDrawer(GravityCompat.START)

View file

@ -16,8 +16,6 @@
package im.vector.riotx.features.home
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import arrow.core.Option
import com.airbnb.mvrx.ActivityViewModelContext
import com.airbnb.mvrx.MvRxState
@ -25,11 +23,9 @@ import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext
import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session
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.create.CreateRoomParams
import im.vector.matrix.rx.rx
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.group.ALL_COMMUNITIES_GROUP_ID
@ -61,10 +57,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
}
private val _isLoading = MutableLiveData<Boolean>()
val isLoading: LiveData<Boolean>
get() = _isLoading
init {
session.addListener(this)
observeRoomAndGroup()
@ -93,7 +85,7 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.filter { !it.isDirect }
.filter {
selectedGroup?.groupId == ALL_COMMUNITIES_GROUP_ID
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
|| selectedGroup?.roomIds?.contains(it.roomId) ?: true
}
filteredDirectRooms + filteredGroupRooms
}
@ -104,21 +96,6 @@ class HomeActivityViewModel @AssistedInject constructor(@Assisted initialState:
.disposeOnClear()
}
fun createRoom(createRoomParams: CreateRoomParams = CreateRoomParams()) {
_isLoading.value = true
session.createRoom(createRoomParams, object : MatrixCallback<String> {
override fun onSuccess(data: String) {
_isLoading.value = false
}
override fun onFailure(failure: Throwable) {
_isLoading.value = false
super.onFailure(failure)
}
})
}
override fun onCleared() {
super.onCleared()

View file

@ -51,8 +51,6 @@ data class HomeDetailParams(
) : Parcelable
private const val CURRENT_DISPLAY_MODE = "CURRENT_DISPLAY_MODE"
private const val INDEX_CATCHUP = 0
private const val INDEX_PEOPLE = 1
private const val INDEX_ROOMS = 2
@ -61,7 +59,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
private val params: HomeDetailParams by args()
private val unreadCounterBadgeViews = arrayListOf<UnreadCounterBadgeView>()
private lateinit var currentDisplayMode: RoomListFragment.DisplayMode
private val viewModel: HomeDetailViewModel by fragmentViewModel()
private lateinit var navigationViewModel: HomeNavigationViewModel
@ -80,15 +77,16 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
currentDisplayMode = savedInstanceState?.getSerializable(CURRENT_DISPLAY_MODE) as? RoomListFragment.DisplayMode
?: RoomListFragment.DisplayMode.HOME
navigationViewModel = ViewModelProviders.of(requireActivity()).get(HomeNavigationViewModel::class.java)
switchDisplayMode(currentDisplayMode)
setupBottomNavigationView()
setupToolbar()
setupKeysBackupBanner()
viewModel.selectSubscribe(this, HomeDetailViewState::displayMode) { displayMode ->
switchDisplayMode(displayMode)
}
}
private fun setupKeysBackupBanner() {
@ -126,11 +124,6 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putSerializable(CURRENT_DISPLAY_MODE, currentDisplayMode)
super.onSaveInstanceState(outState)
}
private fun setupToolbar() {
val parentActivity = vectorBaseActivity
if (parentActivity is ToolbarConfigurable) {
@ -156,10 +149,7 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
R.id.bottom_action_rooms -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
if (currentDisplayMode != displayMode) {
currentDisplayMode = displayMode
switchDisplayMode(displayMode)
}
viewModel.switchDisplayMode(displayMode)
true
}
@ -176,6 +166,12 @@ class HomeDetailFragment : VectorBaseFragment(), KeysBackupBanner.Delegate {
private fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) {
groupToolbarTitleView.setText(displayMode.titleRes)
updateSelectedFragment(displayMode)
// Update the navigation view (for when we restore the tabs)
bottomNavigationView.selectedItemId = when (displayMode) {
RoomListFragment.DisplayMode.PEOPLE -> R.id.bottom_action_people
RoomListFragment.DisplayMode.ROOMS -> R.id.bottom_action_rooms
else -> R.id.bottom_action_home
}
}
private fun updateSelectedFragment(displayMode: RoomListFragment.DisplayMode) {

View file

@ -23,14 +23,19 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.session.Session
import im.vector.matrix.rx.rx
import im.vector.riotx.core.di.HasScreenInjector
import im.vector.riotx.core.platform.VectorViewModel
import im.vector.riotx.features.home.room.list.RoomListFragment
import im.vector.riotx.features.ui.UiStateRepository
import io.reactivex.schedulers.Schedulers
/**
* View model used to update the home bottom bar notification counts
* View model used to update the home bottom bar notification counts, observe the sync state and
* change the selected room list view
*/
class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: HomeDetailViewState,
private val session: Session,
private val uiStateRepository: UiStateRepository,
private val homeRoomListStore: HomeRoomListObservableStore)
: VectorViewModel<HomeDetailViewState>(initialState) {
@ -41,6 +46,13 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
companion object : MvRxViewModelFactory<HomeDetailViewModel, HomeDetailViewState> {
override fun initialState(viewModelContext: ViewModelContext): HomeDetailViewState? {
val uiStateRepository = (viewModelContext.activity as HasScreenInjector).injector().uiStateRepository()
return HomeDetailViewState(
displayMode = uiStateRepository.getDisplayMode()
)
}
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: HomeDetailViewState): HomeDetailViewModel? {
val fragment: HomeDetailFragment = (viewModelContext as FragmentViewModelContext).fragment()
@ -53,6 +65,16 @@ class HomeDetailViewModel @AssistedInject constructor(@Assisted initialState: Ho
observeRoomSummaries()
}
fun switchDisplayMode(displayMode: RoomListFragment.DisplayMode) = withState { state ->
if (state.displayMode != displayMode) {
setState {
copy(displayMode = displayMode)
}
uiStateRepository.storeDisplayMode(displayMode)
}
}
// PRIVATE METHODS *****************************************************************************
private fun observeSyncState() {

View file

@ -18,8 +18,10 @@ package im.vector.riotx.features.home
import com.airbnb.mvrx.MvRxState
import im.vector.matrix.android.api.session.sync.SyncState
import im.vector.riotx.features.home.room.list.RoomListFragment
data class HomeDetailViewState(
val displayMode: RoomListFragment.DisplayMode = RoomListFragment.DisplayMode.HOME,
val notificationCountCatchup: Int = 0,
val notificationHighlightCatchup: Boolean = false,
val notificationCountPeople: Int = 0,

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.riotx.features.home.room.detail
data class FileTooBigError(
val filename: String,
val fileSizeInBytes: Long,
val homeServerLimitInBytes: Long
)

View file

@ -27,7 +27,6 @@ import android.os.Bundle
import android.os.Parcelable
import android.text.Editable
import android.text.Spannable
import android.text.TextUtils
import android.view.*
import android.view.inputmethod.InputMethodManager
import android.widget.TextView
@ -79,20 +78,9 @@ import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.platform.VectorBaseFragment
import im.vector.riotx.core.ui.views.JumpToReadMarkerView
import im.vector.riotx.core.ui.views.NotificationAreaView
import im.vector.riotx.core.utils.*
import im.vector.riotx.core.utils.Debouncer
import im.vector.riotx.core.utils.PERMISSIONS_FOR_TAKING_PHOTO
import im.vector.riotx.core.utils.PERMISSIONS_FOR_WRITING_FILES
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_DOWNLOAD_FILE
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_CAMERA
import im.vector.riotx.core.utils.PERMISSION_REQUEST_CODE_LAUNCH_NATIVE_VIDEO_CAMERA
import im.vector.riotx.core.utils.allGranted
import im.vector.riotx.core.utils.checkPermissions
import im.vector.riotx.core.utils.copyToClipboard
import im.vector.riotx.core.utils.createUIHandler
import im.vector.riotx.core.utils.openCamera
import im.vector.riotx.core.utils.shareMedia
import im.vector.riotx.core.utils.toast
import im.vector.riotx.features.autocomplete.command.AutocompleteCommandPresenter
import im.vector.riotx.features.autocomplete.command.CommandAutocompletePolicy
import im.vector.riotx.features.autocomplete.user.AutocompleteUserPresenter
@ -163,18 +151,15 @@ class RoomDetailFragment :
* @param displayName the display name to sanitize
* @return the sanitized display name
*/
fun sanitizeDisplayname(displayName: String): String? {
// sanity checks
if (!TextUtils.isEmpty(displayName)) {
val ircPattern = " (IRC)"
if (displayName.endsWith(ircPattern)) {
return displayName.substring(0, displayName.length - ircPattern.length)
}
private fun sanitizeDisplayName(displayName: String): String {
if (displayName.endsWith(ircPattern)) {
return displayName.substring(0, displayName.length - ircPattern.length)
}
return displayName
}
private const val ircPattern = " (IRC)"
}
@ -255,6 +240,10 @@ class RoomDetailFragment :
}
}
roomDetailViewModel.fileTooBigEvent.observeEvent(this) {
displayFileTooBigWarning(it)
}
roomDetailViewModel.selectSubscribe(this, RoomDetailViewState::tombstoneEventHandling, uniqueOnly("tombstoneEventHandling")) {
renderTombstoneEventHandling(it)
}
@ -305,6 +294,18 @@ class RoomDetailFragment :
jumpToReadMarkerView.callback = this
}
private fun displayFileTooBigWarning(error: FileTooBigError) {
AlertDialog.Builder(requireActivity())
.setTitle(R.string.dialog_title_error)
.setMessage(getString(R.string.error_file_too_big,
error.filename,
TextUtils.formatFileSize(requireContext(), error.fileSizeInBytes),
TextUtils.formatFileSize(requireContext(), error.homeServerLimitInBytes)
))
.setPositiveButton(R.string.ok, null)
.show()
}
private fun setupNotificationView() {
notificationAreaView.delegate = object : NotificationAreaView.Delegate {
@ -1089,23 +1090,23 @@ class RoomDetailFragment :
// var vibrate = false
val myDisplayName = session.getUser(session.myUserId)?.displayName
if (TextUtils.equals(myDisplayName, text)) {
if (myDisplayName == text) {
// current user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
if (composerLayout.composerEditText.text.isBlank()) {
composerLayout.composerEditText.append(Command.EMOTE.command + " ")
composerLayout.composerEditText.setSelection(composerLayout.composerEditText.text.length)
// vibrate = true
}
} else {
// another user
if (TextUtils.isEmpty(composerLayout.composerEditText.text)) {
if (composerLayout.composerEditText.text.isBlank()) {
// Ensure displayName will not be interpreted as a Slash command
if (text.startsWith("/")) {
composerLayout.composerEditText.append("\\")
}
composerLayout.composerEditText.append(sanitizeDisplayname(text)!! + ": ")
composerLayout.composerEditText.append(sanitizeDisplayName(text) + ": ")
} else {
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayname(text)!! + " ")
composerLayout.composerEditText.text.insert(composerLayout.composerEditText.selectionStart, sanitizeDisplayName(text) + " ")
}
// vibrate = true
@ -1156,5 +1157,4 @@ class RoomDetailFragment :
roomDetailViewModel.process(RoomDetailActions.MarkAllAsRead)
}
}

View file

@ -37,6 +37,7 @@ import im.vector.matrix.android.api.session.events.model.isImageMessage
import im.vector.matrix.android.api.session.events.model.isTextMessage
import im.vector.matrix.android.api.session.events.model.toModel
import im.vector.matrix.android.api.session.file.FileService
import im.vector.matrix.android.api.session.homeserver.HomeServerCapabilities
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.api.session.room.model.message.MessageContent
import im.vector.matrix.android.api.session.room.model.message.MessageType
@ -49,6 +50,7 @@ import im.vector.matrix.android.api.util.Optional
import im.vector.matrix.android.internal.crypto.attachments.toElementToDecrypt
import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventContent
import im.vector.matrix.rx.rx
import im.vector.riotx.BuildConfig
import im.vector.riotx.R
import im.vector.riotx.core.extensions.postLiveEvent
import im.vector.riotx.core.intent.getFilenameFromUri
@ -239,23 +241,22 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
val navigateToEvent: LiveData<LiveEvent<String>>
get() = _navigateToEvent
private val _fileTooBigEvent = MutableLiveData<LiveEvent<FileTooBigError>>()
val fileTooBigEvent: LiveData<LiveEvent<FileTooBigError>>
get() = _fileTooBigEvent
private val _downloadedFileEvent = MutableLiveData<LiveEvent<DownloadFileState>>()
val downloadedFileEvent: LiveData<LiveEvent<DownloadFileState>>
get() = _downloadedFileEvent
fun isMenuItemVisible(@IdRes itemId: Int): Boolean {
if (itemId == R.id.clear_message_queue) {
//For now always disable, woker cancellation is not working properly
return false//timeline.pendingEventCount() > 0
}
if (itemId == R.id.resend_all) {
return timeline.failedToDeliverEventCount() > 0
}
if (itemId == R.id.clear_all) {
return timeline.failedToDeliverEventCount() > 0
}
return false
fun isMenuItemVisible(@IdRes itemId: Int) = when (itemId) {
R.id.clear_message_queue ->
/* For now always disable on production, worker cancellation is not working properly */
timeline.pendingEventCount() > 0 && BuildConfig.DEBUG
R.id.resend_all -> timeline.failedToDeliverEventCount() > 0
R.id.clear_all -> timeline.failedToDeliverEventCount() > 0
else -> false
}
// PRIVATE METHODS *****************************************************************************
@ -482,7 +483,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
type = ContentAttachmentData.Type.values()[it.mediaType]
)
}
room.sendMedias(attachments)
val homeServerCapabilities = session.getHomeServerCapabilities()
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
if (maxUploadFileSize == HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN) {
// Unknown limitation
room.sendMedias(attachments)
} else {
when (val tooBigFile = attachments.find { it.size > maxUploadFileSize }) {
null -> room.sendMedias(attachments)
else -> _fileTooBigEvent.postValue(LiveEvent(FileTooBigError(tooBigFile.name ?: tooBigFile.path, tooBigFile.size, maxUploadFileSize)))
}
}
}
private fun handleEventVisible(action: RoomDetailActions.TimelineEventTurnsVisible) {

View file

@ -38,6 +38,8 @@ import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventDi
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotx.features.home.room.detail.timeline.helper.nextOrNull
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.item.*
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.features.media.VideoContentRenderer
@ -48,6 +50,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineItemFactory: TimelineItemFactory,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val avatarRenderer: AvatarRenderer,
private val dimensionConverter: DimensionConverter,
@TimelineEventControllerHandler
private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {

View file

@ -47,6 +47,10 @@ import im.vector.riotx.core.linkify.VectorLinkify
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.core.resources.StringProvider
import im.vector.riotx.core.utils.DebouncedClickListener
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.containsOnlyEmojis
import im.vector.riotx.core.utils.isLocalFile
import im.vector.riotx.features.home.AvatarRenderer
import im.vector.riotx.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.riotx.features.home.room.detail.timeline.helper.AvatarSizeProvider
@ -131,6 +135,8 @@ class MessageItemFactory @Inject constructor(
attributes: AbsMessageItem.Attributes): MessageFileItem? {
return MessageFileItem_()
.attributes(attributes)
.izLocalFile(messageContent.getFileUrl().isLocalFile())
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight)
.leftGuideline(avatarSizeProvider.leftGuideline)
.filename(messageContent.body)
@ -149,6 +155,8 @@ class MessageItemFactory @Inject constructor(
return MessageFileItem_()
.attributes(attributes)
.leftGuideline(avatarSizeProvider.leftGuideline)
.izLocalFile(messageContent.getFileUrl().isLocalFile())
.contentUploadStateTrackerBinder(contentUploadStateTrackerBinder)
.highlighted(highlight)
.filename(messageContent.body)
.iconRes(R.drawable.filetype_attachment)

View file

@ -16,7 +16,6 @@
package im.vector.riotx.features.home.room.detail.timeline.helper
import android.text.format.Formatter
import android.view.View
import android.view.ViewGroup
import android.widget.ProgressBar
@ -26,23 +25,25 @@ import im.vector.matrix.android.api.session.content.ContentUploadStateTracker
import im.vector.matrix.android.api.session.room.send.SendState
import im.vector.riotx.R
import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.error.ErrorFormatter
import im.vector.riotx.core.resources.ColorProvider
import im.vector.riotx.features.media.ImageContentRenderer
import im.vector.riotx.core.utils.TextUtils
import im.vector.riotx.features.ui.getMessageTextColor
import javax.inject.Inject
class ContentUploadStateTrackerBinder @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val colorProvider: ColorProvider) {
private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter) {
private val updateListeners = mutableMapOf<String, ContentUploadStateTracker.UpdateListener>()
fun bind(eventId: String,
mediaData: ImageContentRenderer.Data,
isLocalFile: Boolean,
progressLayout: ViewGroup) {
activeSessionHolder.getActiveSession().also { session ->
val uploadStateTracker = session.contentUploadProgressTracker()
val updateListener = ContentMediaProgressUpdater(progressLayout, mediaData, colorProvider)
val updateListener = ContentMediaProgressUpdater(progressLayout, isLocalFile, colorProvider, errorFormatter)
updateListeners[eventId] = updateListener
uploadStateTracker.track(eventId, updateListener)
}
@ -60,8 +61,9 @@ class ContentUploadStateTrackerBinder @Inject constructor(private val activeSess
}
private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
private val mediaData: ImageContentRenderer.Data,
private val colorProvider: ColorProvider) : ContentUploadStateTracker.UpdateListener {
private val isLocalFile: Boolean,
private val colorProvider: ColorProvider,
private val errorFormatter: ErrorFormatter) : ContentUploadStateTracker.UpdateListener {
override fun onUpdate(state: ContentUploadStateTracker.State) {
when (state) {
@ -76,7 +78,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
}
private fun handleIdle(state: ContentUploadStateTracker.State.Idle) {
if (mediaData.isLocalFile()) {
if (isLocalFile) {
progressLayout.isVisible = true
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
@ -124,8 +126,8 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
progressBar?.isIndeterminate = false
progressBar?.progress = percent.toInt()
progressTextView?.text = progressLayout.context.getString(resId,
Formatter.formatShortFileSize(progressLayout.context, current),
Formatter.formatShortFileSize(progressLayout.context, total))
TextUtils.formatFileSize(progressLayout.context, current, true),
TextUtils.formatFileSize(progressLayout.context, total, true))
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.SENDING))
}
@ -134,7 +136,7 @@ private class ContentMediaProgressUpdater(private val progressLayout: ViewGroup,
val progressBar = progressLayout.findViewById<ProgressBar>(R.id.mediaProgressBar)
val progressTextView = progressLayout.findViewById<TextView>(R.id.mediaProgressTextView)
progressBar?.isVisible = false
progressTextView?.text = state.throwable.localizedMessage
progressTextView?.text = errorFormatter.toHumanReadable(state.throwable)
progressTextView?.setTextColor(colorProvider.getMessageTextColor(SendState.UNDELIVERED))
}

View file

@ -116,6 +116,7 @@ abstract class AbsMessageItem<H : AbsMessageItem.Holder> : BaseEventItem<H>() {
if (!shouldShowReactionAtBottom() || attributes.informationData.orderedReactionList.isNullOrEmpty()) {
holder.reactionWrapper?.isVisible = false
} else {
//inflate if needed
if (holder.reactionFlowHelper == null) {

View file

@ -56,7 +56,7 @@ abstract class DefaultItem : BaseEventItem<DefaultItem.Holder>() {
}
override fun getEventIds(): List<String> {
return informationData.eventId
return listOf(informationData.eventId)
}
override fun getViewType() = STUB_ID

View file

@ -22,9 +22,11 @@ import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
import im.vector.riotx.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
@EpoxyModelClass(layout = R.layout.item_timeline_event_base)
abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
@ -36,19 +38,34 @@ abstract class MessageFileItem : AbsMessageItem<MessageFileItem.Holder>() {
var iconRes: Int = 0
@EpoxyAttribute
var clickListener: View.OnClickListener? = null
@EpoxyAttribute
var izLocalFile = false
@EpoxyAttribute
lateinit var contentUploadStateTrackerBinder: ContentUploadStateTrackerBinder
override fun bind(holder: Holder) {
super.bind(holder)
renderSendState(holder.fileLayout, holder.filenameView)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, izLocalFile, holder.progressLayout)
} else {
holder.progressLayout.isVisible = false
}
holder.filenameView.text = filename
holder.fileImageView.setImageResource(iconRes)
holder.filenameView.setOnClickListener(clickListener)
holder.filenameView.paintFlags = (holder.filenameView.paintFlags or Paint.UNDERLINE_TEXT_FLAG)
}
override fun unbind(holder: Holder) {
super.unbind(holder)
contentUploadStateTrackerBinder.unbind(attributes.informationData.eventId)
}
override fun getViewType() = STUB_ID
class Holder : AbsMessageItem.Holder(STUB_ID) {
val progressLayout by bind<ViewGroup>(R.id.messageFileUploadProgressLayout)
val fileLayout by bind<ViewGroup>(R.id.messageFileLayout)
val fileImageView by bind<ImageView>(R.id.messageFileImageView)
val filenameView by bind<TextView>(R.id.messageFilenameView)

View file

@ -20,6 +20,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible
import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass
import im.vector.riotx.R
@ -44,11 +45,13 @@ abstract class MessageImageVideoItem : AbsMessageItem<MessageImageVideoItem.Hold
super.bind(holder)
imageContentRenderer.render(mediaData, ImageContentRenderer.Mode.THUMBNAIL, holder.imageView)
if (!attributes.informationData.sendState.hasFailed()) {
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData, holder.progressLayout)
contentUploadStateTrackerBinder.bind(attributes.informationData.eventId, mediaData.isLocalFile(), holder.progressLayout)
} else {
holder.progressLayout.isVisible = false
}
holder.imageView.setOnClickListener(clickListener)
holder.imageView.setOnLongClickListener(attributes.itemLongClickListener)
ViewCompat.setTransitionName(holder.imageView,"imagePreview_${id()}")
ViewCompat.setTransitionName(holder.imageView, "imagePreview_${id()}")
holder.mediaContentView.setOnClickListener(attributes.itemClickListener)
holder.mediaContentView.setOnLongClickListener(attributes.itemLongClickListener)
// The sending state color will be apply to the progress text

View file

@ -33,9 +33,9 @@ import im.vector.riotx.core.di.ActiveSessionHolder
import im.vector.riotx.core.glide.GlideApp
import im.vector.riotx.core.glide.GlideRequest
import im.vector.riotx.core.utils.DimensionConverter
import im.vector.riotx.core.utils.isLocalFile
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
import java.io.File
import javax.inject.Inject
class ImageContentRenderer @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
@ -54,9 +54,7 @@ class ImageContentRenderer @Inject constructor(private val activeSessionHolder:
val rotation: Int? = null
) : Parcelable {
fun isLocalFile(): Boolean {
return url != null && File(url).exists()
}
fun isLocalFile() = url.isLocalFile()
}
enum class Mode {

View file

@ -20,7 +20,7 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.text.Editable
import android.text.TextUtils
import android.util.Patterns
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
@ -171,7 +171,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
MXSession.getApplicationSizeCaches(activity, object : SimpleApiCallback<Long>() {
override fun onSuccess(size: Long) {
if (null != activity) {
it.summary = android.text.format.Formatter.formatFileSize(activity, size)
it.summary = TextUtils.formatFileSize(activity, size)
}
}
})
@ -189,7 +189,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
val size = getSizeOfFiles(requireContext(),
File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
it.summary = android.text.format.Formatter.formatFileSize(activity, size.toLong())
it.summary = TextUtils.formatFileSize(requireContext(), size.toLong())
it.onPreferenceClickListener = Preference.OnPreferenceClickListener {
GlobalScope.launch(Dispatchers.Main) {
@ -208,7 +208,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
File(requireContext().cacheDir, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR))
}
it.summary = android.text.format.Formatter.formatFileSize(activity, newSize.toLong())
it.summary = TextUtils.formatFileSize(requireContext(), newSize.toLong())
hideLoadingView()
}
@ -534,7 +534,7 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
private fun addEmail(email: String) {
// check first if the email syntax is valid
// if email is null , then also its invalid email
if (TextUtils.isEmpty(email) || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
if (email.isBlank() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
activity?.toast(R.string.auth_invalid_email)
return
}
@ -719,9 +719,9 @@ class VectorSettingsGeneralFragment : VectorSettingsBaseFragment() {
val newPwd = newPasswordText.text.toString().trim()
val newConfirmPwd = confirmNewPasswordText.text.toString().trim()
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && TextUtils.equals(newPwd, newConfirmPwd)
updateButton.isEnabled = oldPwd.isNotEmpty() && newPwd.isNotEmpty() && newPwd == newConfirmPwd
if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && !TextUtils.equals(newPwd, newConfirmPwd)) {
if (newPwd.isNotEmpty() && newConfirmPwd.isNotEmpty() && newPwd != newConfirmPwd) {
confirmNewPasswordTil.error = getString(R.string.passwords_do_not_match)
}
}

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.riotx.features.ui
import android.content.SharedPreferences
import androidx.core.content.edit
import im.vector.riotx.features.home.room.list.RoomListFragment
import javax.inject.Inject
/**
* This class is used to persist UI state across application restart
*/
class SharedPreferencesUiStateRepository @Inject constructor(private val sharedPreferences: SharedPreferences) : UiStateRepository {
override fun getDisplayMode(): RoomListFragment.DisplayMode {
return when (sharedPreferences.getInt(KEY_DISPLAY_MODE, VALUE_DISPLAY_MODE_CATCHUP)) {
VALUE_DISPLAY_MODE_PEOPLE -> RoomListFragment.DisplayMode.PEOPLE
VALUE_DISPLAY_MODE_ROOMS -> RoomListFragment.DisplayMode.ROOMS
else -> RoomListFragment.DisplayMode.HOME
}
}
override fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode) {
sharedPreferences.edit {
putInt(KEY_DISPLAY_MODE,
when (displayMode) {
RoomListFragment.DisplayMode.PEOPLE -> VALUE_DISPLAY_MODE_PEOPLE
RoomListFragment.DisplayMode.ROOMS -> VALUE_DISPLAY_MODE_ROOMS
else -> VALUE_DISPLAY_MODE_CATCHUP
})
}
}
companion object {
private const val KEY_DISPLAY_MODE = "UI_STATE_DISPLAY_MODE"
private const val VALUE_DISPLAY_MODE_CATCHUP = 0
private const val VALUE_DISPLAY_MODE_PEOPLE = 1
private const val VALUE_DISPLAY_MODE_ROOMS = 2
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotx.features.ui
import im.vector.riotx.features.home.room.list.RoomListFragment
/**
* This interface is used to persist UI state across application restart
*/
interface UiStateRepository {
fun getDisplayMode(): RoomListFragment.DisplayMode
fun storeDisplayMode(displayMode: RoomListFragment.DisplayMode)
}

View file

@ -2,66 +2,68 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/messageFileLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp">
<LinearLayout
android:id="@+id/messageFileLayout"
<ImageView
android:id="@+id/messageFilee2eIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/e2e_verified"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
<!-- the media type -->
<ImageView
android:id="@+id/messageFileImageView"
android:layout_width="@dimen/chat_avatar_size"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
app:layout_constraintStart_toEndOf="@+id/messageFilee2eIcon"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/filetype_image" />
<!-- the media -->
<TextView
android:id="@+id/messageFilenameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="8dp"
android:duplicateParentState="true"
android:orientation="horizontal"
android:autoLink="none"
android:gravity="center_vertical"
android:minHeight="@dimen/chat_avatar_size"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:layout_constraintStart_toEndOf="@+id/messageFileImageView"
app:layout_constraintTop_toTopOf="parent"
tools:text="A filename here" />
<ImageView
android:id="@+id/messageFilee2eIcon"
android:layout_width="14dp"
android:layout_height="14dp"
android:src="@drawable/e2e_verified"
android:visibility="gone" />
<!-- the media type -->
<ImageView
android:id="@+id/messageFileImageView"
android:layout_width="@dimen/chat_avatar_size"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:src="@drawable/filetype_image" />
<!-- the media -->
<TextView
android:id="@+id/messageFilenameView"
android:layout_width="0dp"
android:layout_height="@dimen/chat_avatar_size"
android:layout_marginStart="4dp"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:autoLink="none"
android:gravity="center_vertical"
tools:text="A filename here" />
</LinearLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/horizontalBarrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="messageFileImageView,messageFilenameView" />
<include
android:id="@+id/messageMediaUploadProgressLayout"
android:id="@+id/messageFileUploadProgressLayout"
layout="@layout/media_upload_download_progress_layout"
android:layout_width="0dp"
android:layout_height="46dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="32dp"
android:layout_marginRight="32dp"
android:layout_marginBottom="8dp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/messageFileLayout"
app:layout_constraintTop_toBottomOf="@+id/horizontalBarrier"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -22,7 +22,7 @@
<item
android:id="@+id/clear_message_queue"
android:title="@string/clear_timeline_send_queue"
android:visible="false"
android:visible="@bool/debug_mode"
app:showAsAction="never"
tools:visible="true" />

View file

@ -22,4 +22,6 @@
<string name="a11y_create_room">Create a new room</string>
<string name="a11y_close_keys_backup_banner">Close keys backup banner</string>
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
</resources>