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
This commit is contained in:
Konrad Pozniak 2019-08-28 19:54:46 +02:00 committed by GitHub
parent 2278fa5c79
commit 42a6b98d4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 154 additions and 88 deletions

View File

@ -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()

View File

@ -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<Status, StatusViewData.Concrete>) {
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<DeletedStatus> {
return timelineCases.delete(id)
}
fun retryAllSearches() {

View File

@ -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<Pair<Status, StatusViewData.Concre
.setMessage(R.string.dialog_redraft_toot_warning)
.setPositiveButton(android.R.string.ok) { _, _ ->
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<Status.Mention>): 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()
}
}

View File

@ -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 <http://www.gnu.org/licenses>. */
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<Attachment>?,
val poll: Poll?,
@SerializedName("created_at") val createdAt: Date
) {
fun isEmpty(): Boolean {
return text == null && attachments == null;
}
}

View File

@ -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

View File

@ -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);

View File

@ -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<ResponseBody> deleteStatus(@Path("id") String statusId);
Single<DeletedStatus> deleteStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/reblog")
Single<Status> reblogStatus(@Path("id") String statusId);
@ -363,11 +364,6 @@ public interface MastodonApi {
@Field("expires_in") String expiresIn
);
@GET("api/v1/filters/{id}")
Call<Filter> getFilter(
@Path("id") String id
);
@FormUrlEncoded
@PUT("api/v1/filters/{id}")
Call<Filter> updateFilter(

View File

@ -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<Status>
fun mute(id: String)
fun block(id: String)
fun delete(id: String)
fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean)
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
@ -101,14 +101,11 @@ class TimelineCasesImpl(
}
override fun delete(id: String) {
val call = mastodonApi.deleteStatus(id)
call.enqueue(object : Callback<ResponseBody> {
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {}
override fun onFailure(call: Call<ResponseBody>, t: Throwable) {}
})
eventHub.dispatch(StatusDeletedEvent(id))
override fun delete(id: String): Single<DeletedStatus> {
return mastodonApi.deleteStatus(id)
.doAfterSuccess {
eventHub.dispatch(StatusDeletedEvent(id))
}
}
override fun pin(status: Status, pin: Boolean) {