From abfd3240bd2c82a335c7e3caf021a6ac80fdb565 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Wed, 30 Oct 2024 14:33:16 +0100 Subject: [PATCH] fix: Don't lose images / captions when editing with failed uploads (#1054) Previous code would remove image attachments from the compose editor if there was a problem uploading or updating them. This caused a particular problem with image captions. You could attach a valid image, then write a caption that was too long for the server. The server would reject the status, and the status was saved to drafts. Then you open the draft, which tries to upload the image again with a too-long caption. The upload is rejected, and the image, along with the caption, is removed. Fix this. - Change `QueuedMedia` to track the upload state as a `Result<_,_>`, so any error messages are preserved and available to the UI. - The different `Ok` types for the upload state contain the upload progress percentage (if appropriate) or the server's ID for the uploaded media. - Change `ProgressImageView` to accept the upload state `Result`. If the result is an error the image is drawn with a red overlay and white "error" icon. - If an upload is in an error state allow the user to click on it. That shows a dialog explaining the error, and provides options to edit the image, change the caption, etc. - When changing the caption make the API call to change it on the server (if the attachment has been uploaded). This makes the user aware of any errors sooner in the process, so they can correct them. Fixes #879 --- .../components/compose/ComposeActivity.kt | 82 ++++++++------ .../components/compose/ComposeViewModel.kt | 91 ++++++++------- .../components/compose/MediaPreviewAdapter.kt | 49 ++++++-- .../components/compose/MediaUploader.kt | 107 +++++++++++------- .../compose/dialog/CaptionDialog.kt | 11 +- .../compose/view/ProgressImageView.kt | 59 ++++++++-- .../app/pachli/service/SendStatusService.kt | 26 ++--- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-cy/strings.xml | 3 +- app/src/main/res/values-de/strings.xml | 3 +- app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-en-rGB/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 4 +- app/src/main/res/values-fa/strings.xml | 3 +- app/src/main/res/values-fi/strings.xml | 4 +- app/src/main/res/values-fr/strings.xml | 3 +- app/src/main/res/values-gd/strings.xml | 3 +- app/src/main/res/values-gl/strings.xml | 4 +- app/src/main/res/values-in/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 3 +- app/src/main/res/values-ja/strings.xml | 3 +- app/src/main/res/values-kab/strings.xml | 1 - app/src/main/res/values-my/strings.xml | 3 +- app/src/main/res/values-nb-rNO/strings.xml | 4 +- app/src/main/res/values-nl/strings.xml | 3 +- app/src/main/res/values-oc/strings.xml | 3 +- app/src/main/res/values-pt-rBR/strings.xml | 3 +- app/src/main/res/values-sv/strings.xml | 3 +- app/src/main/res/values-tr/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 3 +- app/src/main/res/values-vi/strings.xml | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 3 +- app/src/main/res/values/strings.xml | 7 +- .../core/network/retrofit/MastodonApi.kt | 8 +- core/network/src/main/res/values/strings.xml | 4 +- 35 files changed, 293 insertions(+), 222 deletions(-) diff --git a/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt b/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt index 45904447b..ec6bf31af 100644 --- a/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt +++ b/app/src/main/java/app/pachli/components/compose/ComposeActivity.kt @@ -109,7 +109,9 @@ import app.pachli.util.setDrawableTint import com.canhub.cropper.CropImage import com.canhub.cropper.CropImageContract import com.canhub.cropper.options +import com.github.michaelbull.result.Result import com.github.michaelbull.result.getOrElse +import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.onFailure import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.color.MaterialColors @@ -253,7 +255,7 @@ class ComposeActivity : val mediaAdapter = MediaPreviewAdapter( this, onAddCaption = { item -> - CaptionDialog.newInstance(item.localId, item.description, item.uri).show(supportFragmentManager, "caption_dialog") + CaptionDialog.newInstance(item.localId, item.serverId, item.description, item.uri).show(supportFragmentManager, "caption_dialog") }, onAddFocus = { item -> makeFocusDialog(item.focus, item.uri) { newFocus -> @@ -524,14 +526,6 @@ class ComposeActivity : enablePollButton(media.isEmpty()) }.collect() } - - lifecycleScope.launch { - viewModel.uploadError.collect { mediaUploaderError -> - val message = mediaUploaderError.fmt(this@ComposeActivity) - - displayPermamentMessage(getString(R.string.error_media_upload_sending_fmt, message)) - } - } } /** @return List of states of the different bottomsheets */ @@ -1272,14 +1266,8 @@ class ComposeActivity : * User is editing a new post, and can either save the changes as a draft or discard them. */ private fun getSaveAsDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder { - val warning = if (viewModel.media.value.isNotEmpty()) { - R.string.compose_save_draft_loses_media - } else { - R.string.compose_save_draft - } - - return AlertDialog.Builder(this) - .setMessage(warning) + val builder = AlertDialog.Builder(this) + .setTitle(R.string.compose_save_draft) .setPositiveButton(R.string.action_save) { _, _ -> viewModel.stopUploads() saveDraftAndFinish(contentText, contentWarning) @@ -1288,6 +1276,12 @@ class ComposeActivity : viewModel.stopUploads() deleteDraftAndFinish() } + + if (viewModel.media.value.isNotEmpty()) { + builder.setMessage(R.string.compose_save_draft_loses_media) + } + + return builder } /** @@ -1295,14 +1289,8 @@ class ComposeActivity : * discard them. */ private fun getUpdateDraftOrDiscardDialog(contentText: String, contentWarning: String): AlertDialog.Builder { - val warning = if (viewModel.media.value.isNotEmpty()) { - R.string.compose_save_draft_loses_media - } else { - R.string.compose_save_draft - } - - return AlertDialog.Builder(this) - .setMessage(warning) + val builder = AlertDialog.Builder(this) + .setTitle(R.string.compose_save_draft) .setPositiveButton(R.string.action_save) { _, _ -> viewModel.stopUploads() saveDraftAndFinish(contentText, contentWarning) @@ -1311,6 +1299,12 @@ class ComposeActivity : viewModel.stopUploads() finish() } + + if (viewModel.media.value.isNotEmpty()) { + builder.setMessage(R.string.compose_save_draft_loses_media) + } + + return builder } /** @@ -1392,23 +1386,39 @@ class ComposeActivity : val uri: Uri, val type: Type, val mediaSize: Long, - val uploadPercent: Int = 0, - val id: String? = null, val description: String? = null, val focus: Attachment.Focus? = null, - val state: State, + val uploadState: Result, ) { enum class Type { IMAGE, VIDEO, AUDIO, } - enum class State { - UPLOADING, - UNPROCESSED, - PROCESSED, - PUBLISHED, - } + + /** + * Server's ID for this attachment. May be null if the media is still + * being uploaded, or it was uploaded and there was an error that + * meant it couldn't be processed. Attachments that have an error + * *after* processing have a non-null `serverId`. + */ + val serverId: String? + get() = uploadState.mapBoth( + { state -> + when (state) { + is UploadState.Uploading -> null + is UploadState.Uploaded.Processing -> state.serverId + is UploadState.Uploaded.Processed -> state.serverId + is UploadState.Uploaded.Published -> state.serverId + } + }, + { error -> + when (error) { + is MediaUploaderError.UpdateMediaError -> error.serverId + else -> null + } + }, + ) } override fun onTimeSet(time: Date?) { @@ -1425,8 +1435,8 @@ class ComposeActivity : scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } - override fun onUpdateDescription(localId: Int, description: String) { - viewModel.updateDescription(localId, description) + override fun onUpdateDescription(localId: Int, serverId: String?, description: String) { + viewModel.updateDescription(localId, serverId, description) } companion object { diff --git a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt index 94534f5f6..87a862eaf 100644 --- a/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/app/pachli/components/compose/ComposeViewModel.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.viewModelScope import app.pachli.R import app.pachli.components.compose.ComposeActivity.QueuedMedia import app.pachli.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult +import app.pachli.components.compose.UploadState.Uploaded import app.pachli.components.drafts.DraftHelper import app.pachli.components.search.SearchType import app.pachli.core.common.PachliError @@ -52,20 +53,20 @@ import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.Err import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Result +import com.github.michaelbull.result.andThen import com.github.michaelbull.result.get import com.github.michaelbull.result.getOrElse import com.github.michaelbull.result.mapBoth import com.github.michaelbull.result.mapError +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.lifecycle.HiltViewModel import io.github.z4kn4fein.semver.constraints.toConstraint import java.util.Date import javax.inject.Inject import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -144,8 +145,6 @@ class ComposeViewModel @Inject constructor( private val _media: MutableStateFlow> = MutableStateFlow(emptyList()) val media = _media.asStateFlow() - private val _uploadError = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - val uploadError = _uploadError.asSharedFlow() private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE) val closeConfirmation = _closeConfirmation.asStateFlow() private val _statusLength = MutableStateFlow(0) @@ -227,7 +226,7 @@ class ComposeViewModel @Inject constructor( mediaSize = mediaSize, description = description, focus = focus, - state = QueuedMedia.State.UPLOADING, + uploadState = Ok(UploadState.Uploading(percentage = 0)), ) stashMediaItem = mediaItem @@ -245,35 +244,8 @@ class ComposeViewModel @Inject constructor( viewModelScope.launch { mediaUploader .uploadMedia(mediaItem, instanceInfo.value) - .collect { event -> - val item = media.value.find { it.localId == mediaItem.localId } - ?: return@collect - var newMediaItem: QueuedMedia? = null - val uploadEvent = event.getOrElse { - _media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } - _uploadError.emit(it) - return@collect - } - - newMediaItem = when (uploadEvent) { - is UploadEvent.ProgressEvent -> item.copy(uploadPercent = uploadEvent.percentage) - is UploadEvent.FinishedEvent -> { - item.copy( - id = uploadEvent.media.mediaId, - uploadPercent = -1, - state = if (uploadEvent.media.processed) { - QueuedMedia.State.PROCESSED - } else { - QueuedMedia.State.UNPROCESSED - }, - ) - } - } - newMediaItem.let { - _media.update { mediaList -> - mediaList.map { mediaItem -> if (mediaItem.localId == it.localId) it else mediaItem } - } - } + .collect { uploadResult -> + updateMediaItem(mediaItem.localId) { it.copy(uploadState = uploadResult) } } } @@ -288,11 +260,9 @@ class ComposeViewModel @Inject constructor( uri = uri, type = type, mediaSize = 0, - uploadPercent = -1, - id = id, description = description, focus = focus, - state = QueuedMedia.State.PUBLISHED, + uploadState = Ok(Uploaded.Published(id)), ) mediaList + mediaItem } @@ -457,11 +427,11 @@ class ComposeViewModel @Inject constructor( val attachedMedia = media.value.map { item -> MediaToSend( localId = item.localId, - id = item.id, + id = item.serverId, uri = item.uri.toString(), description = item.description, focus = item.focus, - processed = item.state == QueuedMedia.State.PROCESSED || item.state == QueuedMedia.State.PUBLISHED, + processed = item.uploadState.get() is Uploaded.Processed || item.uploadState.get() is Uploaded.Published, ) } val tootToSend = StatusToSend( @@ -498,16 +468,45 @@ class ComposeViewModel @Inject constructor( } } - fun updateDescription(localId: Int, description: String) { - updateMediaItem(localId) { mediaItem -> - mediaItem.copy(description = description) + fun updateDescription(localId: Int, serverId: String?, description: String) { + // If the image hasn't been uploaded then update the state locally. + if (serverId == null) { + updateMediaItem(localId) { mediaItem -> mediaItem.copy(description = description) } + return + } + + // Update the remote description and report any errors. Update the local description + // if there are errors so the user still has the text and can try and correct it. + viewModelScope.launch { + api.updateMedia(serverId, description = description) + .andThen { api.getMedia(serverId) } + .onSuccess { response -> + val state = if (response.code == 200) { + Uploaded.Processed(serverId) + } else { + Uploaded.Processing(serverId) + } + updateMediaItem(localId) { + it.copy( + description = description, + uploadState = Ok(state), + ) + } + } + .mapError { MediaUploaderError.UpdateMediaError(serverId, it) } + .onFailure { error -> + updateMediaItem(localId) { + it.copy( + description = description, + uploadState = Err(error), + ) + } + } } } fun updateFocus(localId: Int, focus: Attachment.Focus) { - updateMediaItem(localId) { mediaItem -> - mediaItem.copy(focus = focus) - } + updateMediaItem(localId) { mediaItem -> mediaItem.copy(focus = focus) } } suspend fun searchAutocompleteSuggestions(token: String): List { diff --git a/app/src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt index c37957017..1b7e4a601 100644 --- a/app/src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/app/pachli/components/compose/MediaPreviewAdapter.kt @@ -21,30 +21,44 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.PopupMenu +import androidx.appcompat.app.AlertDialog import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.AsyncListDiffer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import app.pachli.R +import app.pachli.components.compose.ComposeActivity.QueuedMedia +import app.pachli.components.compose.UploadState.Uploaded import app.pachli.components.compose.view.ProgressImageView import app.pachli.core.designsystem.R as DR import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.github.michaelbull.result.get +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess class MediaPreviewAdapter( context: Context, - private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, - private val onAddFocus: (ComposeActivity.QueuedMedia) -> Unit, - private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit, - private val onRemove: (ComposeActivity.QueuedMedia) -> Unit, + private val onAddCaption: (QueuedMedia) -> Unit, + private val onAddFocus: (QueuedMedia) -> Unit, + private val onEditImage: (QueuedMedia) -> Unit, + private val onRemove: (QueuedMedia) -> Unit, ) : RecyclerView.Adapter() { - fun submitList(list: List) { + fun submitList(list: List) { this.differ.submitList(list) } private fun onMediaClick(position: Int, view: View) { val item = differ.currentList[position] + + // Handle error + item.uploadState + .onSuccess { showMediaPopup(item, view) } + .onFailure { showMediaError(item, it, view) } + } + + private fun showMediaPopup(item: QueuedMedia, view: View) { val popup = PopupMenu(view.context, view) val addCaptionId = 1 val addFocusId = 2 @@ -52,9 +66,9 @@ class MediaPreviewAdapter( val removeId = 4 popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) - if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) { + if (item.type == QueuedMedia.Type.IMAGE) { popup.menu.add(0, addFocusId, 0, R.string.action_set_focus) - if (item.state != ComposeActivity.QueuedMedia.State.PUBLISHED) { + if (item.uploadState.get() !is Uploaded.Published) { // Already-published items can't be edited popup.menu.add(0, editImageId, 0, R.string.action_edit_image) } @@ -72,6 +86,15 @@ class MediaPreviewAdapter( popup.show() } + private fun showMediaError(item: QueuedMedia, error: MediaUploaderError, view: View) { + AlertDialog.Builder(view.context) + .setTitle(R.string.action_post_failed) + .setMessage(view.context.getString(R.string.upload_failed_msg_fmt, error.fmt(view.context))) + .setPositiveButton(android.R.string.ok) { _, _ -> } + .setNegativeButton(R.string.upload_failed_modify_attachment) { _, _ -> showMediaPopup(item, view) } + .show() + } + private val thumbnailViewSize = context.resources.getDimensionPixelSize(DR.dimen.compose_media_preview_size) @@ -84,8 +107,10 @@ class MediaPreviewAdapter( override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) { val item = differ.currentList[position] holder.progressImageView.setChecked(!item.description.isNullOrEmpty()) - holder.progressImageView.setProgress(item.uploadPercent) - if (item.type == ComposeActivity.QueuedMedia.Type.AUDIO) { + + holder.progressImageView.setResult(item.uploadState) + + if (item.type == QueuedMedia.Type.AUDIO) { // TODO: Fancy waveform display? holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) } else { @@ -114,12 +139,12 @@ class MediaPreviewAdapter( private val differ = AsyncListDiffer( this, - object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: QueuedMedia, newItem: QueuedMedia): Boolean { return oldItem.localId == newItem.localId } - override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean { + override fun areContentsTheSame(oldItem: QueuedMedia, newItem: QueuedMedia): Boolean { return oldItem == newItem } }, diff --git a/app/src/main/java/app/pachli/components/compose/MediaUploader.kt b/app/src/main/java/app/pachli/components/compose/MediaUploader.kt index f0c7b7f53..d5188b59b 100644 --- a/app/src/main/java/app/pachli/components/compose/MediaUploader.kt +++ b/app/src/main/java/app/pachli/components/compose/MediaUploader.kt @@ -29,6 +29,7 @@ import app.pachli.BuildConfig import app.pachli.R import app.pachli.components.compose.ComposeActivity.QueuedMedia import app.pachli.components.compose.MediaUploaderError.PrepareMediaError +import app.pachli.components.compose.UploadState.Uploaded import app.pachli.core.common.PachliError import app.pachli.core.common.string.randomAlphanumericString import app.pachli.core.common.util.formatNumber @@ -57,6 +58,7 @@ import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow @@ -68,18 +70,6 @@ import okio.sink import okio.source import timber.log.Timber -/** - * Media that has been fully uploaded to the server and may still be being - * processed. - * - * @property mediaId Server-side identifier for this media item - * @property processed True if the server has finished processing this media item - */ -data class UploadedMedia( - val mediaId: String, - val processed: Boolean, -) - /** * Media that has been prepared for uploading. * @@ -164,44 +154,66 @@ sealed interface MediaUploaderError : PachliError { } } - /** [ApiError] wrapper. */ + /** + * An error occurred uploading the media, and there is no remote ID. + * + * [ApiError] wrapper. + */ @JvmInline value class UploadMediaError(private val error: ApiError) : MediaUploaderError, PachliError by error + /** + * An error occurred updating media that has already been uploaded. + * + * @param serverId Server's ID for the media + */ + data class UpdateMediaError(val serverId: String, val error: ApiError) : MediaUploaderError, PachliError by error + /** Server did return media with ID [uploadId]. */ data class UploadIdNotFoundError(val uploadId: Int) : MediaUploaderError { override val resourceId = R.string.error_media_uploader_upload_not_found_fmt override val formatArgs = arrayOf(uploadId.toString()) override val cause = null } - - /** Catch-all for arbitrary throwables */ - data class ThrowableError(private val throwable: Throwable) : MediaUploaderError { - override val resourceId = R.string.error_media_uploader_throwable_fmt - override val formatArgs = arrayOf(throwable.localizedMessage ?: "") - override val cause = null - } } -/** Events that happen over the life of a media upload. */ -sealed interface UploadEvent { +/** State of a media upload. */ +sealed interface UploadState { /** - * Upload has made progress. + * Upload is in progress, but incomplete. * * @property percentage What percent of the file has been uploaded. */ - data class ProgressEvent(val percentage: Int) : UploadEvent + data class Uploading(val percentage: Int) : UploadState - /** - * Upload has finished. - * - * @property media The uploaded media - */ - data class FinishedEvent(val media: UploadedMedia) : UploadEvent + sealed interface Uploaded : UploadState { + val serverId: String + + /** + * Upload has completed, but the server is still processing the media. + * + * @property serverId Server-side identifier for this media item + */ + data class Processing(override val serverId: String) : UploadState.Uploaded + + /** + * Upload has completed, and the server has processed the media. + * + * @property serverId Server-side identifier for this media item + */ + data class Processed(override val serverId: String) : UploadState.Uploaded + + /** + * Post has been published, editing is impossible. + * + * @property serverId Server-side identifier for this media item + */ + data class Published(override val serverId: String) : UploadState.Uploaded + } } data class UploadData( - val flow: Flow>, + val flow: Flow>, val scope: CoroutineScope, ) @@ -231,28 +243,29 @@ class MediaUploader @Inject constructor( return mostRecentId++ } - suspend fun getMediaUploadState(localId: Int): Result { - return uploads[localId]?.flow - // Can't use filterIsInstance> here because the type - // inside Ok<...> is erased, so the first Ok<_> result is returned, crashing with a - // class cast error if it's a ProgressEvent. - // Kotlin doesn't warn about this, see - // https://discuss.kotlinlang.org/t/is-as-operators-are-unsafe-for-reified-types/22470 - ?.first { it.get() is UploadEvent.FinishedEvent } as? Ok + /** + * Waits for the upload with [localId] to finish (Ok state is one of the + * [Uploaded][UploadState.Uploaded] types), or return an error. + */ + suspend fun waitForUploadToFinish(localId: Int): Result { + return uploads[localId]?.flow?.filter { + it.get() is Uploaded || it.get() == null + }?.first() as? Result ?: Err(MediaUploaderError.UploadIdNotFoundError(localId)) } /** * Uploads media. + * * @param media the media to upload * @param instanceInfo info about the current media to make sure the media gets resized correctly * @return A Flow emitting upload events. * The Flow is hot, in order to cancel upload or clear resources call [cancelUploadScope]. */ @OptIn(ExperimentalCoroutinesApi::class) - fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow> { + fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow> { val uploadScope = CoroutineScope(Dispatchers.IO) - val uploadFlow: Flow> = flow { + val uploadFlow: Flow> = flow { if (shouldResizeMedia(media, instanceInfo)) { emit(downsize(media, instanceInfo)) } else { @@ -371,7 +384,7 @@ class MediaUploader @Inject constructor( private val contentResolver = context.contentResolver - private suspend fun upload(media: QueuedMedia): Flow> { + private suspend fun upload(media: QueuedMedia): Flow> { return callbackFlow { var mimeType = contentResolver.getType(media.uri) @@ -405,7 +418,7 @@ class MediaUploader @Inject constructor( media.mediaSize, ) { percentage -> if (percentage != lastProgress) { - trySend(Ok(UploadEvent.ProgressEvent(percentage))) + trySend(Ok(UploadState.Uploading(percentage))) } lastProgress = percentage } @@ -424,7 +437,13 @@ class MediaUploader @Inject constructor( val uploadResult = mediaUploadApi.uploadMedia(body, description, focus) .mapEither( - { UploadEvent.FinishedEvent(UploadedMedia(it.body.id, it.code == 200)) }, + { + if (it.code == 200) { + Uploaded.Processed(it.body.id) + } else { + Uploaded.Processing(it.body.id) + } + }, { MediaUploaderError.UploadMediaError(it) }, ) send(uploadResult) diff --git a/app/src/main/java/app/pachli/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/app/pachli/components/compose/dialog/CaptionDialog.kt index 1acc3d1b9..c007e5593 100644 --- a/app/src/main/java/app/pachli/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/app/pachli/components/compose/dialog/CaptionDialog.kt @@ -68,10 +68,12 @@ class CaptionDialog : DialogFragment() { input.requestFocus() val localId = arguments?.getInt(ARG_LOCAL_ID) ?: error("Missing localId") + val serverId = arguments?.getString(ARG_SERVER_ID) + val dialog = AlertDialog.Builder(context) .setView(binding.root) .setPositiveButton(android.R.string.ok) { _, _ -> - listener.onUpdateDescription(localId, input.text.toString()) + listener.onUpdateDescription(localId, serverId, input.text.toString()) } .setNegativeButton(android.R.string.cancel, null) .create() @@ -125,22 +127,25 @@ class CaptionDialog : DialogFragment() { } interface Listener { - fun onUpdateDescription(localId: Int, description: String) + fun onUpdateDescription(localId: Int, serverId: String?, description: String) } companion object { private const val KEY_DESCRIPTION = "app.pachli.KEY_DESCRIPTION" + private const val ARG_LOCAL_ID = "app.pachli.ARG_LOCAL_ID" + private const val ARG_SERVER_ID = "app.pachli.ARG_SERVER_ID" private const val ARG_EXISTING_DESCRIPTION = "app.pachli.ARG_EXISTING_DESCRIPTION" private const val ARG_PREVIEW_URI = "app.pachli.ARG_PREVIEW_URI" - private const val ARG_LOCAL_ID = "app.pachli.ARG_LOCAL_ID" fun newInstance( localId: Int, + serverId: String? = null, existingDescription: String?, previewUri: Uri, ) = CaptionDialog().apply { arguments = bundleOf( ARG_LOCAL_ID to localId, + ARG_SERVER_ID to serverId, ARG_EXISTING_DESCRIPTION to existingDescription, ARG_PREVIEW_URI to previewUri, ) diff --git a/app/src/main/java/app/pachli/components/compose/view/ProgressImageView.kt b/app/src/main/java/app/pachli/components/compose/view/ProgressImageView.kt index 1fc2daa19..9e8ac10eb 100644 --- a/app/src/main/java/app/pachli/components/compose/view/ProgressImageView.kt +++ b/app/src/main/java/app/pachli/components/compose/view/ProgressImageView.kt @@ -24,12 +24,22 @@ import android.graphics.PorterDuff import android.graphics.PorterDuffXfermode import android.graphics.RectF import android.util.AttributeSet +import androidx.annotation.OptIn import androidx.appcompat.content.res.AppCompatResources import app.pachli.R +import app.pachli.components.compose.MediaUploaderError +import app.pachli.components.compose.UploadState import app.pachli.core.designsystem.R as DR +import app.pachli.core.ui.makeIcon import app.pachli.view.MediaPreviewImageView import at.connyduck.sparkbutton.helpers.Utils +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess +import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.color.MaterialColors +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial class ProgressImageView @JvmOverloads constructor( @@ -37,7 +47,7 @@ class ProgressImageView attrs: AttributeSet? = null, defStyleAttr: Int = 0, ) : MediaPreviewImageView(context, attrs, defStyleAttr) { - private var progress = -1 + private var result: Result = Ok(UploadState.Uploading(0)) private val progressRect = RectF() private val biggerRect = RectF() private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { @@ -60,14 +70,15 @@ class ProgressImageView } private val circleRadius = Utils.dpToPx(context, 14) private val circleMargin = Utils.dpToPx(context, 14) + private val uploadErrorRadius = Utils.dpToPx(context, 24) - fun setProgress(progress: Int) { - this.progress = progress - if (progress != -1) { - setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY) - } else { - clearColorFilter() - } + private val uploadErrorDrawable = makeIcon(context, GoogleMaterial.Icon.gmd_error, 48).apply { + setTint(Color.WHITE) + } + + @OptIn(ExperimentalBadgeUtils::class) + fun setResult(result: Result) { + this.result = result invalidate() } @@ -82,6 +93,24 @@ class ProgressImageView override fun onDraw(canvas: Canvas) { super.onDraw(canvas) + + result.onSuccess { value -> + val percentage = when (value) { + is UploadState.Uploading -> value.percentage + else -> -1 + } + onDrawSuccess(canvas, percentage) + }.onFailure { error -> + onDrawError(canvas) + } + } + + private fun onDrawSuccess(canvas: Canvas, progress: Int) { + clearColorFilter() + if (progress != -1) { + setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY) + } + val angle = progress / 100f * 360 - 90 val halfWidth = width / 2f val halfHeight = height / 2f @@ -107,4 +136,18 @@ class ProgressImageView ) captionDrawable.draw(canvas) } + + private fun onDrawError(canvas: Canvas) { + setColorFilter( + MaterialColors.getColor(this, androidx.appcompat.R.attr.colorError), + PorterDuff.Mode.DARKEN, + ) + uploadErrorDrawable.setBounds( + (width / 2) - uploadErrorRadius, + (height / 2) - uploadErrorRadius, + (width / 2) + uploadErrorRadius, + (height / 2) + uploadErrorRadius, + ) + uploadErrorDrawable.draw(canvas) + } } diff --git a/app/src/main/java/app/pachli/service/SendStatusService.kt b/app/src/main/java/app/pachli/service/SendStatusService.kt index 604ab011d..b0b9ab118 100644 --- a/app/src/main/java/app/pachli/service/SendStatusService.kt +++ b/app/src/main/java/app/pachli/service/SendStatusService.kt @@ -38,6 +38,8 @@ import app.pachli.core.network.model.Status import app.pachli.core.network.retrofit.MastodonApi import at.connyduck.calladapter.networkresult.fold import com.github.michaelbull.result.getOrElse +import com.github.michaelbull.result.onFailure +import com.github.michaelbull.result.onSuccess import dagger.hilt.android.AndroidEntryPoint import java.io.IOException import java.util.Date @@ -145,14 +147,14 @@ class SendStatusService : Service() { // first, wait for media uploads to finish val media = statusToSend.media.map { mediaItem -> if (mediaItem.id == null) { - val uploadState = mediaUploader.getMediaUploadState(mediaItem.localId) + val uploadState = mediaUploader.waitForUploadToFinish(mediaItem.localId) val media = uploadState.getOrElse { Timber.w("failed uploading media: %s", it.fmt(this@SendStatusService)) failSending(statusId) stopSelfWhenDone() return@launch - }.media - mediaItem.copy(id = media.mediaId, processed = media.processed) + } + mediaItem.copy(id = media.serverId) } else { mediaItem } @@ -165,15 +167,13 @@ class SendStatusService : Service() { delay(1000L * mediaCheckRetries) media.forEach { mediaItem -> if (!mediaItem.processed) { - when (mastodonApi.getMedia(mediaItem.id!!).code()) { - 200 -> mediaItem.processed = true // success - 206 -> { } // media is still being processed, continue checking - else -> { // some kind of server error, retrying probably doesn't make sense + mastodonApi.getMedia(mediaItem.id!!) + .onSuccess { mediaItem.processed = it.code == 200 } + .onFailure { failSending(statusId) stopSelfWhenDone() return@launch } - } } } mediaCheckRetries++ @@ -190,13 +190,11 @@ class SendStatusService : Service() { media.forEach { mediaItem -> if (mediaItem.processed && (mediaItem.description != null || mediaItem.focus != null)) { mastodonApi.updateMedia(mediaItem.id!!, mediaItem.description, mediaItem.focus?.toMastodonApiString()) - .fold({ - }, { throwable -> - Timber.w(throwable, "failed to update media on status send") - failOrRetry(throwable, statusId) - + .onFailure { error -> + Timber.w("failed to update media on status send: %s", error) + failOrRetry(error.throwable, statusId) return@launch - }) + } } } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 13413675e..1d96dca87 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -590,7 +590,6 @@ %1$s :فشل إجراء إضافة المنشور إلى الفواصل المرجعية غير معروف دائما - اخفق التحميل: %s أبدًا %s (الكلمةكاملة) إضافة كلمة مفتاحية diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 04aa7d577..c20211857 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -612,8 +612,7 @@ \nHwn yw mater Mastodon #25398. Llwytho hysbysiadau diweddaraf Dileu\'r drafft\? - Methodd y lanlwytho: %s Methodd chwarae: %s Dileu Dileu\'r hidlydd \'%1$s\'\? - \ No newline at end of file + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 9cb620a81..20ab7351e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -568,7 +568,6 @@ Deinem Server ist bekannt, dass dieser Beitrag bearbeitet wurde. Allerdings besitzt er keine Kopien der Änderungen, weshalb diese nicht angezeigt werden können. \n \nHierbei handelt es sich um Mastodon Issue #25398. - Das Hochladen ist fehlgeschlagen: %s Übersetzung fehlgeschlagen: %1$s Erinnere mich nie Übersetze… @@ -603,4 +602,4 @@ %1$s %2$d %1$s %2$s (Aktualisiert: %1$s) - \ No newline at end of file + diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index fb8a54a90..23cff3b44 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -93,7 +93,6 @@ Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης; Σφάλμα ακολουθίας #%s Σφάλμα μη-ακολουθίας #%s - Το ανέβασμα απέτυχε: %s Το ανέβασμα αρχείου απέτυχε. Αρχική Σελίδα Σφάλμα αποστολής ανάρτησης. diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index b4e93da78..fa8c0aaf2 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -59,7 +59,6 @@ This instance does not support following hashtags. Edits Followed hashtags - The upload failed: %s Re-login for push notifications Drafts Posts @@ -69,4 +68,4 @@ Hashtags Error muting #%s Scheduled posts - \ No newline at end of file + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index e1f2790c7..2a7b071ca 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -592,7 +592,6 @@ Cargar las publicaciones más nuevas ¿Quieres guardar tus cambios de perfil? Siempre - La carga falló: %s Nunca Alguien impulsa su propia publicación La carga de filtros falló: %1$s @@ -655,7 +654,6 @@ El idioma de la publicación está configurado en %1$s, pero es posible que la hayas escrito en %2$s. el servidor no es compatible con el tipo de archivo: %1$s No se pudo adjuntar el archivo a la publicación: %1$s - %1$s al agente de resolución de contenido le faltó una ruta: %1$s el agente de resolución de contenido tiene un esquema no compatible: %1$s el tamaño del archivo es %1$s, el máximo permitido es %2$s @@ -791,4 +789,4 @@ Ningún participante más Revisar idioma de la publicación antes de publicar Publicar como está (%1$s) y no volver a preguntar - \ No newline at end of file + diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 6125ad8ba..4a3ba6808 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -568,8 +568,7 @@ آگاهی‌ها هنگامی که تاسکی در پس‌زمینه کار می‌کند واکشی آگاهی‌ها… نگه‌داری انباره… - بارگذاری شکست خورد: %s پخش شکست خورد: %s حذف «%1$s» حذف پالایهٔ ؟ - \ No newline at end of file + diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 10d1f929e..021fee357 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -300,7 +300,6 @@ Käyttäjän esto poistettu Muokkaukset Seuratut aihetunnisteet - Lataus epäonnistui: %s Lisää uusi Mastodon-tili Mediatiedoston latausta viimeistellään Joku tehostaa omaa julkaisuaan @@ -631,7 +630,6 @@ Merkitse kieleksi %1$s ja julkaise Julkaise muuttamattomana (%1$s) medialatausta nimeltä %1$s ei löydy - %1$s sisällönselvittäjä-URI:ltä puuttui polku: %1$s %1$s tiedoston koko on %1$s, suurin sallittu on %2$s @@ -776,4 +774,4 @@ Tarkasta julkaisun kieli ennen julkaisemista Kopioi kohde Teksti kopioitu - \ No newline at end of file + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 9474832d4..2583ec884 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -572,7 +572,6 @@ Modifier mot-clé %s : %s Annuler la traduction - Le téléversement a échoué : %s Publications en tendance Hashtags Traduire @@ -607,4 +606,4 @@ Récupération des notifications … Maintenance du cache … %1$s %2$s - \ No newline at end of file + diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 5eda9973b..7acce2ba0 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -583,5 +583,4 @@ Brathan nuair a bhios Pachli ag obair sa chùlaibh A’ faighinn nam brathan… Obair-ghlèidhidh air an tasgadan… - Dh’fhàillig leis an luchdadh suas: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index bebd7cbf5..4c7ef1d9a 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -561,7 +561,6 @@ Notificacións cando Pachli está a funcionar en segundo plano Obtendo as notificacións… Mantemento da caché… - Fallou a subida: %s Eliminar Eliminar o filtro \'%1$s\'\? Mostrar autopromocións @@ -628,7 +627,6 @@ Cambiar o idioma a %1$s e publicar non se atopou o multimedia subido con ID %1$s Publicar tal como está (%1$s) - %1$s ao resolutor da URI do contido fáltalle unha ruta: %1$s %1$s o resolutor da URI do cotido non é compatible co esquema: %1$s @@ -770,4 +768,4 @@ ✔ hai %1$s @ %2$s ✖ hai %1$s @ %2$s A conta non ten o método «push». Pechar a sesión e volver acceder podería arranxalo. - \ No newline at end of file + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 449b7d2cc..4ef94cd85 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -210,7 +210,6 @@ Postingan trending Sunting Tagar yang diikuti - Upload gagal: %s Postingan Tagar %s dilaporkan %s diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index f8c3973ae..c865b62a8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -606,9 +606,8 @@ Non ricordarmelo per questa versione Aggiornamenti Software Sempre - Il caricamento è fallito: %s Mai Post Una volta per versione Un aggiornamento è disponibile - \ No newline at end of file + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 702ce7537..4dfe883e9 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -551,7 +551,6 @@ 最新の通知を読み込む 下書きを削除しますか? リストを読み込む際のエラー - アップロードに失敗しました: %s あなたのサーバーは、この投稿が変更されたことを把握していますが、編集履歴のコピーを備えていないので、表示できません。 \n \nこれはMastodonのissue #25398です。 @@ -595,4 +594,4 @@ 翻訳 アップデート可能です 翻訳を元に戻す - \ No newline at end of file + diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index bcd780893..ce401d692 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -277,7 +277,6 @@ War talast Ḍfeṛ Wayeḍ - %1$s Tavidyutt %1$s Tugna diff --git a/app/src/main/res/values-my/strings.xml b/app/src/main/res/values-my/strings.xml index 9c692fe72..46d127edc 100644 --- a/app/src/main/res/values-my/strings.xml +++ b/app/src/main/res/values-my/strings.xml @@ -7,7 +7,6 @@ ဤဖိုင်အမျိုးအစားကိုမတင်နိုင်ပါ။ ဤဖိုင်ကိုမဖွင့်နိုင်ပါ။ ရုပ်ပုံနှင့်ဗီဒီယိုများအား ပိုစ့်တစ်ခုတည်းတွင် ယှဥ်တွဲမတင်နိုင်ပါ။ - တင်ခြင်းမအောင်မြင်ပါ။: %s ပိုစ့်တင်ရာ၌ချို့ယွင်းမှု။ စောင့်ကြည့်ရာ၌ ချို့ယွင်းမှု #%s Mute လုပ်ရာ၌ ချို့ယွင်းချက် #%s @@ -103,4 +102,4 @@ ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။ ပိုစ့်တင်ခြင်းမအောင်မြင်ပါ ပုံကြမ်းများထဲတွင်သိမ်းထားသည်။\n\nဆာဗာအားမချိတ်ဆက်နိုင်ခြင်း သို့ ပယ်ချခြင်းဖြစ်နိုင်သည်။ ဘလော့ခ် - \ No newline at end of file + diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index c043ac0de..a7b0f5fc1 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -575,7 +575,6 @@ Lister - kunne ikke lastes inn Håndter lister %1$s %2$s - Mislyktes ved opplasting: %s Trendende lenker Emneknagger Lenker @@ -586,7 +585,6 @@ Språket til innlegget er %1$s men det kan skje at du har skrevet innlegget på %2$s. Byt språk til %1$s og tut Mediaopplasting med ID %1$s finnes ikke - %1$s Oversettelse mislyktes: %1$s Å laste inn filtere mislyktes: %1$s En moderator suspenderte kontoen @@ -772,4 +770,4 @@ Alle innlegg Dine egne innlegg, fremhevinger, favoritmerker, bokmerker, og innlegg som @nevner deg Fødererte innlegg - \ No newline at end of file + diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 90778f370..847e78122 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -549,7 +549,6 @@ Meldingen ophalen… Achtergrond activiteit Meldingen als Pachli werkt op de achtergrond - De upload is mislukt: %s Contact zoeken met je server duurde te lang Verwijder Verwijder filter \'%1$s\'\? @@ -608,4 +607,4 @@ Er zijn geen updates beschikbaar Volgende geplande controle: %1$s Je server ondersteund geen filters - \ No newline at end of file + diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 619326ce3..c005db416 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -568,5 +568,4 @@ Notificacions quand Tuska s’executa en rèireplan Recuperacion de las notificacions… Manteniment del cache… - Fracàs del mandadís : %s - \ No newline at end of file + diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 161bbdc28..8dbfdb762 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -557,7 +557,6 @@ Desconhecido Uso total Sempre - O carregamento falhou: %s Nunca %s (palavra inteira) Adicionar palavra-chave @@ -614,4 +613,4 @@ Carregar notificações mais recentes Descartar mudanças Tamanho do texto da UI - \ No newline at end of file + diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 45e395693..a3577e65a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -593,7 +593,6 @@ Ladda de nyaste inläggen Vill du spara dina profiländringar? Alltid - Uppladdning misslyckades: %s Aldrig Någon som knuffar sin toot Laddning av filter misslyckades: %1$s @@ -611,4 +610,4 @@ Ångra översättning Kunde inte hämta serverinformation för %1$s: %2$s Din server stöder inte filter - \ No newline at end of file + diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1866a3224..2cf25a225 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -568,6 +568,5 @@ Sunucunuz bu gönderinin düzenlendiğini bilir, ancak düzenlemelerin bir kopyası yoktur, bu nedenle bunlar size gösterilemez. \n \nBu Mastodon sorununu #25398. - Yükleme başarısız oldu: %s Oynatma başarısız oldu: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 4a46d4a46..df7387883 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -583,5 +583,4 @@ Ваш сервер знає, що цей допис було змінено, але не має копії редагувань, тому вони не можуть бути вам показані. \n \nЦе помилка #25398 у Mastodon. - Не вдалося вивантажити: %s - \ No newline at end of file + diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index e46fc4655..59a6c6efd 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -551,8 +551,7 @@ Thông báo khi Pachli hoạt động ngầm Đang nạp thông báo… Bảo trì bộ nhớ đệm… - Không thể tải lên: %s Không thể phát: %s Xóa bộ lọc \'%1$s\'\? Xóa - \ No newline at end of file + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index abd31fbeb..8f2cd4322 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -565,8 +565,7 @@ 获取通知中… 缓存维护… 后台活动 - 上传失败了:%s 播放失败了:%s 删除筛选器\'%1$s\'吗? 删除 - \ No newline at end of file + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9458afe24..1d65488e9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -27,7 +27,6 @@ Permission to store media is required. images and videos cannot both be attached to the same post. The upload failed. - The upload failed: %s Error sending post. Error following #%s Error unfollowing #%s @@ -397,7 +396,7 @@ Requires you to manually approve followers Delete draft? Save draft? - Save draft? (Attachments will be uploaded again when you restore the draft.) + Attachments will be uploaded again when you restore the draft. You have unsaved changes. Sending post… Error sending post @@ -721,7 +720,6 @@ Post as-is (%1$s) and don\'t ask again media upload with ID %1$s not found - %1$s content resolver URI was missing a path: %1$s content resolver URI has unsupported scheme: %1$s @@ -853,4 +851,7 @@ Copy item Text copied + + The upload will be retried when you send the post. If it fails again the post will be saved in your drafts.\n\nThe error was: %1$s + Modify attachment diff --git a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt index e03245664..8ac6510eb 100644 --- a/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt +++ b/core/network/src/main/kotlin/app/pachli/core/network/retrofit/MastodonApi.kt @@ -201,14 +201,14 @@ interface MastodonApi { @PUT("api/v1/media/{mediaId}") suspend fun updateMedia( @Path("mediaId") mediaId: String, - @Field("description") description: String?, - @Field("focus") focus: String?, - ): NetworkResult + @Field("description") description: String? = null, + @Field("focus") focus: String? = null, + ): ApiResult @GET("api/v1/media/{mediaId}") suspend fun getMedia( @Path("mediaId") mediaId: String, - ): Response + ): ApiResult @POST("api/v1/statuses") suspend fun createStatus( diff --git a/core/network/src/main/res/values/strings.xml b/core/network/src/main/res/values/strings.xml index 6ecb876d1..ac5f6a2c0 100644 --- a/core/network/src/main/res/values/strings.xml +++ b/core/network/src/main/res/values/strings.xml @@ -21,8 +21,8 @@ software version is missing, empty, or blank could not parse \"%1$s\" as a version: %2$s - An error occurred: %s - Your server does not support this feature: %1$s + %1$s + your server does not support this feature: %1$s your server is rate-limiting your requests: %1$s Your server returned an invalid response: %1$s A network error occurred: %s