From 42a6b98d4d7d581366bce465082999db7ba6ccfc Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 28 Aug 2019 19:54:46 +0200 Subject: [PATCH] use status source for delete and redraft (#1461) * use status source for delete and redraft * make delete & redraft work on Pleroma again * add error handling --- .../conversation/ConversationsViewModel.kt | 3 + .../components/search/SearchViewModel.kt | 11 ++- .../fragments/SearchStatusesFragment.kt | 61 ++++++++------- .../tusky/entity/DeletedStatus.kt | 34 ++++++++ .../com/keylesspalace/tusky/entity/Status.kt | 31 ++++++++ .../tusky/fragment/SFragment.java | 77 ++++++++++--------- .../tusky/network/MastodonApi.java | 8 +- .../tusky/network/TimelineCases.kt | 17 ++-- 8 files changed, 154 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 212e92cc0..515d34f0e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -115,6 +115,9 @@ class ConversationsViewModel @Inject constructor( /* this is not ideal since deleting last toot from an conversation should not delete the conversation but show another toot of the conversation */ timelineCases.delete(conversation.lastStatus.id) + .subscribeOn(Schedulers.io()) + .doOnError { t -> Log.w("ConversationViewModel", "Failed to delete conversation", t) } + .subscribe() database.conversationDao().delete(conversation) .subscribeOn(Schedulers.io()) .subscribe() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 1b254e1c0..76b5bd494 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -9,16 +9,14 @@ import androidx.paging.PagedList import com.keylesspalace.tusky.components.search.adapter.SearchRepository import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.entity.HashTag -import com.keylesspalace.tusky.entity.Poll -import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.util.Listing import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.ViewDataUtils import com.keylesspalace.tusky.viewdata.StatusViewData +import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import javax.inject.Inject @@ -91,6 +89,7 @@ class SearchViewModel @Inject constructor( fun removeItem(status: Pair) { timelineCases.delete(status.first.id) + .subscribe() if (loadedStatuses.remove(status)) repoResultStatus.value?.refresh?.invoke() } @@ -198,8 +197,8 @@ class SearchViewModel @Inject constructor( timelineCases.block(accountId) } - fun deleteStatus(id: String) { - timelineCases.delete(id) + fun deleteStatus(id: String): Single { + return timelineCases.delete(id) } fun retryAllSearches() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index bada1fa24..707288cac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -25,15 +25,14 @@ import android.content.pm.PackageManager import android.net.Uri import android.os.Environment import android.preference.PreferenceManager -import android.text.SpannableStringBuilder -import android.text.Spanned -import android.text.style.URLSpan +import android.util.Log import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat +import androidx.lifecycle.Lifecycle import androidx.lifecycle.LiveData import androidx.paging.PagedList import androidx.paging.PagedListAdapter @@ -50,6 +49,9 @@ import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.viewdata.AttachmentViewData import com.keylesspalace.tusky.viewdata.StatusViewData +import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from +import com.uber.autodispose.autoDisposable +import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_search.* import java.util.* @@ -389,39 +391,36 @@ class SearchStatusesFragment : SearchFragment viewModel.deleteStatus(id) - removeItem(position) + .observeOn(AndroidSchedulers.mainThread()) + .autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe ({ deletedStatus -> + removeItem(position) + + val redraftStatus = if(deletedStatus.isEmpty()) { + status.toDeletedStatus() + } else { + deletedStatus + } + + val intent = ComposeActivity.IntentBuilder() + .tootText(redraftStatus.text) + .inReplyToId(redraftStatus.inReplyToId) + .visibility(redraftStatus.visibility) + .contentWarning(redraftStatus.spoilerText) + .mediaAttachments(redraftStatus.attachments) + .sensitive(redraftStatus.sensitive) + .poll(redraftStatus.poll?.toNewPoll(status.createdAt)) + .build(context) + startActivity(intent) + }, { error -> + Log.w("SearchStatusesFragment", "error deleting status", error) + Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT).show() + }) - val intent = ComposeActivity.IntentBuilder() - .tootText(getEditableText(status.content, status.mentions)) - .inReplyToId(status.inReplyToId) - .visibility(status.visibility) - .contentWarning(status.spoilerText) - .mediaAttachments(status.attachments) - .sensitive(status.sensitive) - .poll(status.poll?.toNewPoll(status.createdAt)) - .build(context) - startActivity(intent) } .setNegativeButton(android.R.string.cancel, null) .show() } } - private fun getEditableText(content: Spanned, mentions: Array): String { - val builder = SpannableStringBuilder(content) - for (span in content.getSpans(0, content.length, URLSpan::class.java)) { - val url = span.url - for ((_, url1, username) in mentions) { - if (url == url1) { - val start = builder.getSpanStart(span) - val end = builder.getSpanEnd(span) - builder.replace(start, end, "@$username") - break - } - } - } - return builder.toString() - } - - } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt new file mode 100644 index 000000000..a3c775bc0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -0,0 +1,34 @@ +/* Copyright 2019 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.entity + +import com.google.gson.annotations.SerializedName +import java.util.* + +data class DeletedStatus( + var text: String?, + @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("spoiler_text") val spoilerText: String, + val visibility: Status.Visibility, + val sensitive: Boolean, + @SerializedName("media_attachments") var attachments: ArrayList?, + val poll: Poll?, + @SerializedName("created_at") val createdAt: Date +) { + fun isEmpty(): Boolean { + return text == null && attachments == null; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index 202be598c..0563f8540 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -15,7 +15,9 @@ package com.keylesspalace.tusky.entity +import android.text.SpannableStringBuilder import android.text.Spanned +import android.text.style.URLSpan import com.google.gson.annotations.SerializedName import java.util.* @@ -108,6 +110,35 @@ data class Status( return pinned ?: false } + fun toDeletedStatus(): DeletedStatus { + return DeletedStatus( + text = getEditableText(), + inReplyToId = inReplyToId, + spoilerText = spoilerText, + visibility = visibility, + sensitive = sensitive, + attachments = attachments, + poll = poll, + createdAt = createdAt + ) + } + + private fun getEditableText(): String { + val builder = SpannableStringBuilder(content) + for (span in content.getSpans(0, content.length, URLSpan::class.java)) { + val url = span.url + for ((_, url1, username) in mentions) { + if (url == url1) { + val start = builder.getSpanStart(span) + val end = builder.getSpanEnd(span) + builder.replace(start, end, "@$username") + break + } + } + } + return builder.toString() + } + override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || javaClass != other.javaClass) return false diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 1efaf1807..1f2f99f89 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -24,10 +24,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Environment; -import android.text.SpannableStringBuilder; -import android.text.Spanned; import android.text.TextUtils; -import android.text.style.URLSpan; import android.util.Log; import android.view.Menu; import android.view.MenuItem; @@ -40,6 +37,7 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.ViewCompat; +import androidx.lifecycle.Lifecycle; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BottomSheetActivity; @@ -68,10 +66,14 @@ import java.util.regex.Pattern; import javax.inject.Inject; +import io.reactivex.android.schedulers.AndroidSchedulers; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; +import static com.uber.autodispose.AutoDispose.autoDisposable; +import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; + /* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature * of that is complicated by how they're coupled with Status and Notification and the corresponding @@ -347,57 +349,62 @@ public abstract class SFragment extends BaseFragment implements Injectable { new AlertDialog.Builder(getActivity()) .setMessage(R.string.dialog_delete_toot_warning) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - timelineCases.delete(id); + timelineCases.delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + deletedStatus -> {}, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show(); + }); removeItem(position); }) .setNegativeButton(android.R.string.cancel, null) .show(); } - private void showConfirmEditDialog(final String id, final int position, Status status) { + private void showConfirmEditDialog(final String id, final int position, final Status status) { if (getActivity() == null) { return; } new AlertDialog.Builder(getActivity()) .setMessage(R.string.dialog_redraft_toot_warning) .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { - timelineCases.delete(id); - removeItem(position); + timelineCases.delete(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe(deletedStatus -> { + removeItem(position); - ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder() - .tootText(getEditableText(status.getContent(), status.getMentions())) - .inReplyToId(status.getInReplyToId()) - .visibility(status.getVisibility()) - .contentWarning(status.getSpoilerText()) - .mediaAttachments(status.getAttachments()) - .sensitive(status.getSensitive()); - if(status.getPoll() != null) { - intentBuilder.poll(status.getPoll().toNewPoll(status.getCreatedAt())); - } + if(deletedStatus.isEmpty()) { + deletedStatus = status.toDeletedStatus(); + } + + ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder() + .tootText(deletedStatus.getText()) + .inReplyToId(deletedStatus.getInReplyToId()) + .visibility(deletedStatus.getVisibility()) + .contentWarning(deletedStatus.getSpoilerText()) + .mediaAttachments(deletedStatus.getAttachments()) + .sensitive(deletedStatus.getSensitive()); + if(deletedStatus.getPoll() != null) { + intentBuilder.poll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt())); + } + + Intent intent = intentBuilder.build(getContext()); + startActivity(intent); + }, + error -> { + Log.w("SFragment", "error deleting status", error); + Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show(); + }); - Intent intent = intentBuilder.build(getContext()); - startActivity(intent); }) .setNegativeButton(android.R.string.cancel, null) .show(); } - private String getEditableText(Spanned content, Status.Mention[] mentions) { - SpannableStringBuilder builder = new SpannableStringBuilder(content); - for (URLSpan span : content.getSpans(0, content.length(), URLSpan.class)) { - String url = span.getURL(); - for (Status.Mention mention : mentions) { - if (url.equals(mention.getUrl())) { - int start = builder.getSpanStart(span); - int end = builder.getSpanEnd(span); - builder.replace(start, end, '@' + mention.getUsername()); - break; - } - } - } - return builder.toString(); - } - private void openAsAccount(String statusUrl, AccountEntity account) { accountManager.setActiveAccount(account); Intent intent = new Intent(getContext(), MainActivity.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 4f4df9057..4d3a6d8a9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -20,6 +20,7 @@ import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.AppCredentials; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Conversation; +import com.keylesspalace.tusky.entity.DeletedStatus; import com.keylesspalace.tusky.entity.Emoji; import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Instance; @@ -152,7 +153,7 @@ public interface MastodonApi { @Query("max_id") String maxId); @DELETE("api/v1/statuses/{id}") - Call deleteStatus(@Path("id") String statusId); + Single deleteStatus(@Path("id") String statusId); @POST("api/v1/statuses/{id}/reblog") Single reblogStatus(@Path("id") String statusId); @@ -363,11 +364,6 @@ public interface MastodonApi { @Field("expires_in") String expiresIn ); - @GET("api/v1/filters/{id}") - Call getFilter( - @Path("id") String id - ); - @FormUrlEncoded @PUT("api/v1/filters/{id}") Call updateFilter( diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index 9b1ca9dc8..eb59660b9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -16,13 +16,13 @@ package com.keylesspalace.tusky.network import com.keylesspalace.tusky.appstore.* +import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Status import io.reactivex.Single import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.addTo -import okhttp3.ResponseBody import retrofit2.Call import retrofit2.Callback import retrofit2.Response @@ -37,7 +37,7 @@ interface TimelineCases { fun favourite(status: Status, favourite: Boolean): Single fun mute(id: String) fun block(id: String) - fun delete(id: String) + fun delete(id: String): Single fun pin(status: Status, pin: Boolean) fun voteInPoll(status: Status, choices: List): Single @@ -101,14 +101,11 @@ class TimelineCasesImpl( } - override fun delete(id: String) { - val call = mastodonApi.deleteStatus(id) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) {} - - override fun onFailure(call: Call, t: Throwable) {} - }) - eventHub.dispatch(StatusDeletedEvent(id)) + override fun delete(id: String): Single { + return mastodonApi.deleteStatus(id) + .doAfterSuccess { + eventHub.dispatch(StatusDeletedEvent(id)) + } } override fun pin(status: Status, pin: Boolean) {