Merge branch 'develop' into feature/read_marker
This commit is contained in:
commit
a3f561d788
11
CHANGES.md
11
CHANGES.md
|
@ -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)
|
||||
===================================================
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -46,6 +46,7 @@ import io.realm.annotations.RealmModule
|
|||
ReadReceiptsSummaryEntity::class,
|
||||
ReadMarkerEntity::class,
|
||||
UserDraftsEntity::class,
|
||||
DraftEntity::class
|
||||
DraftEntity::class,
|
||||
HomeServerCapabilitiesEntity::class
|
||||
])
|
||||
internal class SessionRealmModule
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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/"
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
* ========================================================================================== */
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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" />
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue