mirror of
https://github.com/element-hq/element-android
synced 2024-11-27 11:59:12 +03:00
Improve file too big error detection and rendering (#3245)
This commit is contained in:
parent
4a23d31271
commit
e108534a2a
18 changed files with 130 additions and 12 deletions
|
@ -8,6 +8,7 @@ Improvements 🙌:
|
|||
- Add ability to install APK from directly from Element (#2381)
|
||||
- Delete and react to stickers (#3250)
|
||||
- Compress video before sending (#442)
|
||||
- Improve file too big error detection (#3245)
|
||||
|
||||
Bugfix 🐛:
|
||||
- Message states cosmetic changes (#3007)
|
||||
|
|
|
@ -28,6 +28,8 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
|||
import org.matrix.android.sdk.internal.crypto.model.event.EncryptedEventContent
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import org.json.JSONObject
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import timber.log.Timber
|
||||
|
||||
typealias Content = JsonDict
|
||||
|
@ -90,6 +92,16 @@ data class Event(
|
|||
@Transient
|
||||
var sendState: SendState = SendState.UNKNOWN
|
||||
|
||||
@Transient
|
||||
var sendStateDetails: String? = null
|
||||
|
||||
fun sendStateError(): MatrixError? {
|
||||
return sendStateDetails?.let {
|
||||
val matrixErrorAdapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||
tryOrNull { matrixErrorAdapter.fromJson(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The `age` value transcoded in a timestamp based on the device clock when the SDK received
|
||||
* the event from the home server.
|
||||
|
|
|
@ -23,6 +23,7 @@ import org.matrix.android.sdk.internal.network.executeRequest
|
|||
import org.matrix.android.sdk.internal.session.room.RoomAPI
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.task.Task
|
||||
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
|
||||
import javax.inject.Inject
|
||||
|
||||
internal interface SendVerificationMessageTask : Task<SendVerificationMessageTask.Params, String> {
|
||||
|
@ -55,7 +56,7 @@ internal class DefaultSendVerificationMessageTask @Inject constructor(
|
|||
localEchoRepository.updateSendState(localId, event.roomId, SendState.SENT)
|
||||
return response.eventId
|
||||
} catch (e: Throwable) {
|
||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(localId, event.roomId, SendState.UNDELIVERED, e.toMatrixErrorStr())
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ import javax.inject.Inject
|
|||
class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
||||
|
||||
companion object {
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 10L
|
||||
const val SESSION_STORE_SCHEMA_VERSION = 11L
|
||||
}
|
||||
|
||||
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
|
||||
|
@ -59,6 +59,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
if (oldVersion <= 7) migrateTo8(realm)
|
||||
if (oldVersion <= 8) migrateTo9(realm)
|
||||
if (oldVersion <= 9) migrateTo10(realm)
|
||||
if (oldVersion <= 10) migrateTo11(realm)
|
||||
}
|
||||
|
||||
private fun migrateTo1(realm: DynamicRealm) {
|
||||
|
@ -163,7 +164,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
?.addRealmListField(EditAggregatedSummaryEntityFields.EDITIONS.`$`, editionOfEventSchema)
|
||||
}
|
||||
|
||||
fun migrateTo9(realm: DynamicRealm) {
|
||||
private fun migrateTo9(realm: DynamicRealm) {
|
||||
Timber.d("Step 8 -> 9")
|
||||
|
||||
realm.schema.get("RoomSummaryEntity")
|
||||
|
@ -201,7 +202,7 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
}
|
||||
}
|
||||
|
||||
fun migrateTo10(realm: DynamicRealm) {
|
||||
private fun migrateTo10(realm: DynamicRealm) {
|
||||
Timber.d("Step 9 -> 10")
|
||||
realm.schema.create("SpaceChildSummaryEntity")
|
||||
?.addField(SpaceChildSummaryEntityFields.ORDER, String::class.java)
|
||||
|
@ -240,4 +241,10 @@ class RealmSessionStoreMigration @Inject constructor() : RealmMigration {
|
|||
?.addRealmListField(RoomSummaryEntityFields.PARENTS.`$`, realm.schema.get("SpaceParentSummaryEntity")!!)
|
||||
?.addRealmListField(RoomSummaryEntityFields.CHILDREN.`$`, realm.schema.get("SpaceChildSummaryEntity")!!)
|
||||
}
|
||||
|
||||
private fun migrateTo11(realm: DynamicRealm) {
|
||||
Timber.d("Step 10 -> 11")
|
||||
realm.schema.get("EventEntity")
|
||||
?.addField(EventEntityFields.SEND_STATE_DETAILS, String::class.java)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,7 @@ internal object EventMapper {
|
|||
).also {
|
||||
it.ageLocalTs = eventEntity.ageLocalTs
|
||||
it.sendState = eventEntity.sendState
|
||||
it.sendStateDetails = eventEntity.sendStateDetails
|
||||
eventEntity.decryptionResultJson?.let { json ->
|
||||
try {
|
||||
it.mxDecryptionResult = MoshiProvider.providesMoshi().adapter(OlmDecryptionResult::class.java).fromJson(json)
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.matrix.android.sdk.internal.crypto.algorithms.olm.OlmDecryptionResult
|
|||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
import io.realm.RealmObject
|
||||
import io.realm.annotations.Index
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
|
||||
internal open class EventEntity(@Index var eventId: String = "",
|
||||
@Index var roomId: String = "",
|
||||
|
@ -32,6 +33,8 @@ internal open class EventEntity(@Index var eventId: String = "",
|
|||
@Index var stateKey: String? = null,
|
||||
var originServerTs: Long? = null,
|
||||
@Index var sender: String? = null,
|
||||
// Can contain a serialized MatrixError
|
||||
var sendStateDetails: String? = null,
|
||||
var age: Long? = 0,
|
||||
var unsignedData: String? = null,
|
||||
var redacts: String? = null,
|
||||
|
|
|
@ -31,12 +31,16 @@ import okhttp3.RequestBody.Companion.toRequestBody
|
|||
import okio.BufferedSink
|
||||
import okio.source
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.api.session.content.ContentUrlResolver
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.internal.di.Authenticated
|
||||
import org.matrix.android.sdk.internal.network.GlobalErrorReceiver
|
||||
import org.matrix.android.sdk.internal.network.ProgressRequestBody
|
||||
import org.matrix.android.sdk.internal.network.awaitResponse
|
||||
import org.matrix.android.sdk.internal.network.toFailure
|
||||
import org.matrix.android.sdk.internal.session.homeserver.DefaultHomeServerCapabilitiesService
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
|
@ -46,6 +50,7 @@ import javax.inject.Inject
|
|||
internal class FileUploader @Inject constructor(@Authenticated
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val globalErrorReceiver: GlobalErrorReceiver,
|
||||
private val homeServerCapabilitiesService: DefaultHomeServerCapabilitiesService,
|
||||
private val context: Context,
|
||||
contentUrlResolver: ContentUrlResolver,
|
||||
moshi: Moshi) {
|
||||
|
@ -57,6 +62,22 @@ internal class FileUploader @Inject constructor(@Authenticated
|
|||
filename: String?,
|
||||
mimeType: String?,
|
||||
progressListener: ProgressRequestBody.Listener? = null): ContentUploadResponse {
|
||||
// Check size limit
|
||||
// DO NOT COMMIT: 5 Mo
|
||||
val maxUploadFileSize = 5 * 1024 * 1024L // homeServerCapabilitiesService.getHomeServerCapabilities().maxUploadFileSize
|
||||
|
||||
if (maxUploadFileSize != HomeServerCapabilities.MAX_UPLOAD_FILE_SIZE_UNKNOWN
|
||||
&& file.length() > maxUploadFileSize) {
|
||||
// Known limitation and file too big for the server, save the pain to upload it
|
||||
throw Failure.ServerError(
|
||||
error = MatrixError(
|
||||
code = MatrixError.M_TOO_LARGE,
|
||||
message = "Cannot upload files larger than ${maxUploadFileSize / 1048576L}mb"
|
||||
),
|
||||
httpCode = 413
|
||||
)
|
||||
}
|
||||
|
||||
val uploadBody = object : RequestBody() {
|
||||
override fun contentLength() = file.length()
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ import org.matrix.android.sdk.internal.session.room.send.CancelSendTracker
|
|||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoIdentifiers
|
||||
import org.matrix.android.sdk.internal.session.room.send.LocalEchoRepository
|
||||
import org.matrix.android.sdk.internal.session.room.send.MultipleEventSendingDispatcherWorker
|
||||
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import org.matrix.android.sdk.internal.worker.WorkerParamsFactory
|
||||
|
@ -129,6 +130,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
}
|
||||
}
|
||||
|
||||
// TODO Send the Thumbnail after the main content, because the main content can fail if too large.
|
||||
val uploadThumbnailResult = dealWithThumbnail(params)
|
||||
|
||||
val progressListener = object : ProgressRequestBody.Listener {
|
||||
|
@ -304,7 +306,7 @@ internal class UploadContentWorker(val context: Context, params: WorkerParameter
|
|||
return Result.success(
|
||||
WorkerParamsFactory.toData(
|
||||
params.copy(
|
||||
lastFailureMessage = failure.localizedMessage
|
||||
lastFailureMessage = failure.toMatrixErrorStr()
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -99,6 +99,7 @@ internal class EventEditor @Inject constructor(private val eventSenderProcessor:
|
|||
entity.age = editedEventEntity.age
|
||||
entity.originServerTs = editedEventEntity.originServerTs
|
||||
entity.sendState = editedEventEntity.sendState
|
||||
entity.sendStateDetails = editedEventEntity.sendStateDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -87,7 +87,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSendState(eventId: String, roomId: String?, sendState: SendState) {
|
||||
fun updateSendState(eventId: String, roomId: String?, sendState: SendState, sendStateDetails: String? = null) {
|
||||
Timber.v("## SendEvent: [${System.currentTimeMillis()}] Update local state of $eventId to ${sendState.name}")
|
||||
timelineInput.onLocalEchoUpdated(roomId = roomId ?: "", eventId = eventId, sendState = sendState)
|
||||
updateEchoAsync(eventId) { realm, sendingEventEntity ->
|
||||
|
@ -96,6 +96,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
} else {
|
||||
sendingEventEntity.sendState = sendState
|
||||
}
|
||||
sendingEventEntity.sendStateDetails = sendStateDetails
|
||||
roomSummaryUpdater.updateSendingInformation(realm, sendingEventEntity.roomId)
|
||||
}
|
||||
}
|
||||
|
@ -161,6 +162,7 @@ internal class LocalEchoRepository @Inject constructor(@SessionDatabase private
|
|||
val timelineEvents = TimelineEventEntity.where(realm, roomId, eventIds).findAll()
|
||||
timelineEvents.forEach {
|
||||
it.root?.sendState = sendState
|
||||
it.root?.sendStateDetails = null
|
||||
}
|
||||
roomSummaryUpdater.updateSendingInformation(realm, roomId)
|
||||
}
|
||||
|
|
|
@ -55,7 +55,12 @@ internal class MultipleEventSendingDispatcherWorker(context: Context, params: Wo
|
|||
|
||||
override fun doOnError(params: Params): Result {
|
||||
params.localEchoIds.forEach { localEchoIds ->
|
||||
localEchoRepository.updateSendState(localEchoIds.eventId, localEchoIds.roomId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(
|
||||
eventId = localEchoIds.eventId,
|
||||
roomId = localEchoIds.roomId,
|
||||
sendState = SendState.UNDELIVERED,
|
||||
sendStateDetails = params.lastFailureMessage
|
||||
)
|
||||
}
|
||||
|
||||
return super.doOnError(params)
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.matrix.android.sdk.api.session.room.send.SendState
|
|||
import org.matrix.android.sdk.internal.crypto.tasks.SendEventTask
|
||||
import org.matrix.android.sdk.internal.di.SessionDatabase
|
||||
import org.matrix.android.sdk.internal.session.SessionComponent
|
||||
import org.matrix.android.sdk.internal.util.toMatrixErrorStr
|
||||
import org.matrix.android.sdk.internal.worker.SessionSafeCoroutineWorker
|
||||
import org.matrix.android.sdk.internal.worker.SessionWorkerParams
|
||||
import timber.log.Timber
|
||||
|
@ -77,7 +78,12 @@ internal class SendEventWorker(context: Context,
|
|||
}
|
||||
|
||||
if (params.lastFailureMessage != null) {
|
||||
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(
|
||||
eventId = event.eventId,
|
||||
roomId = event.roomId,
|
||||
sendState = SendState.UNDELIVERED,
|
||||
sendStateDetails = params.lastFailureMessage
|
||||
)
|
||||
// Transmit the error
|
||||
return Result.success(inputData)
|
||||
.also { Timber.e("Work cancelled due to input error from parent") }
|
||||
|
@ -90,7 +96,12 @@ internal class SendEventWorker(context: Context,
|
|||
} catch (exception: Throwable) {
|
||||
if (/*currentAttemptCount >= MAX_NUMBER_OF_RETRY_BEFORE_FAILING ||**/ !exception.shouldBeRetried()) {
|
||||
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed cannot retry ${params.eventId} > ${exception.localizedMessage}")
|
||||
localEchoRepository.updateSendState(event.eventId, event.roomId, SendState.UNDELIVERED)
|
||||
localEchoRepository.updateSendState(
|
||||
eventId = event.eventId,
|
||||
roomId = event.roomId,
|
||||
sendState = SendState.UNDELIVERED,
|
||||
sendStateDetails = exception.toMatrixErrorStr()
|
||||
)
|
||||
Result.success()
|
||||
} else {
|
||||
Timber.e("## SendEvent: [${System.currentTimeMillis()}] Send event Failed schedule retry ${params.eventId} > ${exception.localizedMessage}")
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright (c) 2021 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* 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 org.matrix.android.sdk.internal.util
|
||||
|
||||
import org.matrix.android.sdk.api.extensions.tryOrNull
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.failure.MatrixError
|
||||
import org.matrix.android.sdk.internal.di.MoshiProvider
|
||||
|
||||
/**
|
||||
* Try to extract and serialize a MatrixError, or default to localizedMessage
|
||||
*/
|
||||
internal fun Throwable.toMatrixErrorStr(): String {
|
||||
return (this as? Failure.ServerError)
|
||||
?.let {
|
||||
// Serialize the MatrixError in this case
|
||||
val adapter = MoshiProvider.providesMoshi().adapter(MatrixError::class.java)
|
||||
tryOrNull { adapter.toJson(error) }
|
||||
}
|
||||
?: localizedMessage
|
||||
?: "error"
|
||||
}
|
|
@ -78,6 +78,9 @@ class DefaultErrorFormatter @Inject constructor(
|
|||
throwable.error.code == MatrixError.M_LIMIT_EXCEEDED -> {
|
||||
limitExceededError(throwable.error)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_TOO_LARGE -> {
|
||||
stringProvider.getString(R.string.error_file_too_big_simple)
|
||||
}
|
||||
throwable.error.code == MatrixError.M_THREEPID_NOT_FOUND -> {
|
||||
stringProvider.getString(R.string.login_reset_password_error_not_found)
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
|
|||
object ShowWaitingView : RoomDetailViewEvents()
|
||||
object HideWaitingView : RoomDetailViewEvents()
|
||||
|
||||
// TODO Remove
|
||||
data class FileTooBigError(
|
||||
val filename: String,
|
||||
val fileSizeInBytes: Long,
|
||||
|
|
|
@ -79,7 +79,6 @@ import org.matrix.android.sdk.api.session.events.model.isTextMessage
|
|||
import org.matrix.android.sdk.api.session.events.model.toContent
|
||||
import org.matrix.android.sdk.api.session.events.model.toModel
|
||||
import org.matrix.android.sdk.api.session.file.FileService
|
||||
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
|
||||
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
|
||||
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
|
||||
import org.matrix.android.sdk.api.session.room.model.Membership
|
||||
|
@ -292,7 +291,6 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
|
||||
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
|
||||
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
|
||||
is RoomDetailAction.ResendAll -> handleResendAll()
|
||||
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
|
||||
is RoomDetailAction.ReportContent -> handleReportContent(action)
|
||||
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
|
||||
|
@ -1107,6 +1105,10 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
}
|
||||
|
||||
private fun handleSendMedia(action: RoomDetailAction.SendMedia) {
|
||||
room.sendMedias(action.attachments, action.compressBeforeSending, emptySet())
|
||||
|
||||
/*
|
||||
TODO Cleanup this error is now managed by the SDK
|
||||
val attachments = action.attachments
|
||||
val homeServerCapabilities = session.getHomeServerCapabilities()
|
||||
val maxUploadFileSize = homeServerCapabilities.maxUploadFileSize
|
||||
|
@ -1124,6 +1126,7 @@ class RoomDetailViewModel @AssistedInject constructor(
|
|||
))
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
private fun handleEventVisible(action: RoomDetailAction.TimelineEventTurnsVisible) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import im.vector.app.core.epoxy.bottomsheet.bottomSheetMessagePreviewItem
|
|||
import im.vector.app.core.epoxy.bottomsheet.bottomSheetQuickReactionsItem
|
||||
import im.vector.app.core.epoxy.bottomsheet.bottomSheetSendStateItem
|
||||
import im.vector.app.core.epoxy.dividerItem
|
||||
import im.vector.app.core.error.ErrorFormatter
|
||||
import im.vector.app.core.resources.StringProvider
|
||||
import im.vector.app.core.utils.DimensionConverter
|
||||
import im.vector.app.features.home.AvatarRenderer
|
||||
|
@ -38,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.tools.createLinkMovement
|
|||
import im.vector.app.features.home.room.detail.timeline.tools.linkify
|
||||
import im.vector.app.features.media.ImageContentRenderer
|
||||
import org.matrix.android.sdk.api.extensions.orFalse
|
||||
import org.matrix.android.sdk.api.failure.Failure
|
||||
import org.matrix.android.sdk.api.session.room.send.SendState
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -50,6 +52,7 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||
private val fontProvider: EmojiCompatFontProvider,
|
||||
private val imageContentRenderer: ImageContentRenderer,
|
||||
private val dimensionConverter: DimensionConverter,
|
||||
private val errorFormatter: ErrorFormatter,
|
||||
private val dateFormatter: VectorDateFormatter
|
||||
) : TypedEpoxyController<MessageActionState>() {
|
||||
|
||||
|
@ -74,10 +77,14 @@ class MessageActionsEpoxyController @Inject constructor(
|
|||
// Send state
|
||||
val sendState = state.sendState()
|
||||
if (sendState?.hasFailed().orFalse()) {
|
||||
// Get more details about the error
|
||||
val errorMessage = state.timelineEvent()?.root?.sendStateError()
|
||||
?.let { errorFormatter.toHumanReadable(Failure.ServerError(it, 0)) }
|
||||
?: stringProvider.getString(R.string.unable_to_send_message)
|
||||
bottomSheetSendStateItem {
|
||||
id("send_state")
|
||||
showProgress(false)
|
||||
text(stringProvider.getString(R.string.unable_to_send_message))
|
||||
text(errorMessage)
|
||||
drawableStart(R.drawable.ic_warning_badge)
|
||||
}
|
||||
} else if (sendState?.isSending().orFalse()) {
|
||||
|
|
|
@ -2295,6 +2295,7 @@
|
|||
<item quantity="other">%d users read</item>
|
||||
</plurals>
|
||||
|
||||
<string name="error_file_too_big_simple">"The file is too large to upload."</string>
|
||||
<string name="error_file_too_big">"The file '%1$s' (%2$s) is too large to upload. The limit is %3$s."</string>
|
||||
|
||||
<string name="error_attachment">"An error occurred while retrieving the attachment."</string>
|
||||
|
|
Loading…
Reference in a new issue