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:
parent
2278fa5c79
commit
42a6b98d4d
8 changed files with 154 additions and 88 deletions
|
@ -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()
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
.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(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))
|
||||
.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()
|
||||
})
|
||||
|
||||
}
|
||||
.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()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
timelineCases.delete(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(deletedStatus -> {
|
||||
removeItem(position);
|
||||
|
||||
if(deletedStatus.isEmpty()) {
|
||||
deletedStatus = status.toDeletedStatus();
|
||||
}
|
||||
|
||||
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()));
|
||||
.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();
|
||||
});
|
||||
|
||||
})
|
||||
.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);
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,15 +101,12 @@ 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) {}
|
||||
})
|
||||
override fun delete(id: String): Single<DeletedStatus> {
|
||||
return mastodonApi.deleteStatus(id)
|
||||
.doAfterSuccess {
|
||||
eventHub.dispatch(StatusDeletedEvent(id))
|
||||
}
|
||||
}
|
||||
|
||||
override fun pin(status: Status, pin: Boolean) {
|
||||
// Replace with extension method if we use RxKotlin
|
||||
|
|
Loading…
Reference in a new issue