Add ability to pin/unpin statuses (#872)

This commit is contained in:
Ivan Kupalov 2018-10-03 21:27:52 +02:00 committed by Konrad Pozniak
parent f6934cadd8
commit a0988dc6c6
9 changed files with 61 additions and 13 deletions

View file

@ -113,6 +113,8 @@ dependencies {
debugImplementation 'im.dino:dbinspector:3.4.1@aar' debugImplementation 'im.dino:dbinspector:3.4.1@aar'
implementation 'io.reactivex.rxjava2:rxjava:2.2.1' implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0' implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.0.0-RC2' implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.0.0-RC2'
implementation 'com.uber.autodispose:autodispose-ktx:1.0.0-RC2' implementation 'com.uber.autodispose:autodispose-ktx:1.0.0-RC2'
} }

View file

@ -36,6 +36,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Converter import retrofit2.Converter
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton import javax.inject.Singleton
@ -93,6 +94,7 @@ class NetworkModule {
converters.fold(builder) { b, c -> converters.fold(builder) { b, c ->
b.addConverterFactory(c) b.addConverterFactory(c)
} }
builder.addCallAdapterFactory(RxJava2CallAdapterFactory.createAsync())
} }
.build() .build()

View file

@ -38,7 +38,8 @@ data class Status(
val visibility: Visibility, val visibility: Visibility,
@SerializedName("media_attachments") var attachments: List<Attachment>, @SerializedName("media_attachments") var attachments: List<Attachment>,
val mentions: Array<Mention>, val mentions: Array<Mention>,
val application: Application? val application: Application?,
var pinned: Boolean?
) { ) {
val actionableId: String? val actionableId: String?

View file

@ -20,6 +20,7 @@ import android.content.ClipboardManager;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
@ -62,6 +63,7 @@ public abstract class SFragment extends BaseFragment {
protected String loggedInUsername; protected String loggedInUsername;
protected abstract TimelineCases timelineCases(); protected abstract TimelineCases timelineCases();
protected abstract void removeItem(int position); protected abstract void removeItem(int position);
protected abstract void onReblog(final boolean reblog, final int position); protected abstract void onReblog(final boolean reblog, final int position);
@ -92,8 +94,8 @@ public abstract class SFragment extends BaseFragment {
@Override @Override
public void onAttach(Context context) { public void onAttach(Context context) {
super.onAttach(context); super.onAttach(context);
if(context instanceof BottomSheetActivity) { if (context instanceof BottomSheetActivity) {
bottomSheetActivity = (BottomSheetActivity)context; bottomSheetActivity = (BottomSheetActivity) context;
} else { } else {
throw new IllegalStateException("Fragment must be attached to a BottomSheetActivity!"); throw new IllegalStateException("Fragment must be attached to a BottomSheetActivity!");
} }
@ -139,7 +141,7 @@ public abstract class SFragment extends BaseFragment {
getActivity().startActivity(intent); getActivity().startActivity(intent);
} }
protected void more(final Status status, View view, final int position) { protected void more(@NonNull final Status status, View view, final int position) {
final String id = status.getActionableId(); final String id = status.getActionableId();
final String accountId = status.getActionableStatus().getAccount().getId(); final String accountId = status.getActionableStatus().getAccount().getId();
final String accountUsename = status.getActionableStatus().getAccount().getUsername(); final String accountUsename = status.getActionableStatus().getAccount().getUsername();
@ -157,6 +159,10 @@ public abstract class SFragment extends BaseFragment {
if (status.getReblog() != null) reblogged = status.getReblog().getReblogged(); if (status.getReblog() != null) reblogged = status.getReblog().getReblogged();
menu.findItem(R.id.status_reblog_private).setVisible(!reblogged); menu.findItem(R.id.status_reblog_private).setVisible(!reblogged);
menu.findItem(R.id.status_unreblog_private).setVisible(reblogged); menu.findItem(R.id.status_unreblog_private).setVisible(reblogged);
} else {
final String textId =
getString(status.getPinned() ? R.string.unpin_action : R.string.pin_action);
menu.add(0, R.id.pin, 1, textId);
} }
} }
popup.setOnMenuItemClickListener(item -> { popup.setOnMenuItemClickListener(item -> {
@ -213,6 +219,10 @@ public abstract class SFragment extends BaseFragment {
showConfirmDeleteDialog(id, position); showConfirmDeleteDialog(id, position);
return true; return true;
} }
case R.id.pin: {
timelineCases().pin(status, !status.getPinned());
return true;
}
} }
return false; return false;
}); });
@ -276,12 +286,12 @@ public abstract class SFragment extends BaseFragment {
protected void showConfirmDeleteDialog(final String id, final int position) { protected void showConfirmDeleteDialog(final String id, final int position) {
new AlertDialog.Builder(getActivity()) new AlertDialog.Builder(getActivity())
.setMessage(R.string.dialog_delete_toot_warning) .setMessage(R.string.dialog_delete_toot_warning)
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { .setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
timelineCases().delete(id); timelineCases().delete(id);
removeItem(position); removeItem(position);
}) })
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show(); .show();
} }
} }

View file

@ -33,6 +33,7 @@ import com.keylesspalace.tusky.entity.StatusContext;
import java.util.List; import java.util.List;
import io.reactivex.Single;
import okhttp3.MultipartBody; import okhttp3.MultipartBody;
import okhttp3.RequestBody; import okhttp3.RequestBody;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
@ -156,6 +157,12 @@ public interface MastodonApi {
@POST("api/v1/statuses/{id}/unfavourite") @POST("api/v1/statuses/{id}/unfavourite")
Call<Status> unfavouriteStatus(@Path("id") String statusId); Call<Status> unfavouriteStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/pin")
Single<Status> pinStatus(@Path("id") String statusId);
@POST("api/v1/statuses/{id}/unpin")
Single<Status> unpinStatus(@Path("id") String statusId);
@GET("api/v1/accounts/verify_credentials") @GET("api/v1/accounts/verify_credentials")
Call<Account> accountVerifyCredentials(); Call<Account> accountVerifyCredentials();

View file

@ -15,12 +15,14 @@
package com.keylesspalace.tusky.network package com.keylesspalace.tusky.network
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.BlockEvent
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.MuteEvent import com.keylesspalace.tusky.appstore.MuteEvent
import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.appstore.StatusDeletedEvent
import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.entity.Relationship
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.rxkotlin.addTo
import okhttp3.ResponseBody import okhttp3.ResponseBody
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
@ -36,12 +38,20 @@ interface TimelineCases {
fun mute(id: String) fun mute(id: String)
fun block(id: String) fun block(id: String)
fun delete(id: String) fun delete(id: String)
fun pin(status: Status, pin: Boolean)
} }
class TimelineCasesImpl( class TimelineCasesImpl(
private val mastodonApi: MastodonApi, private val mastodonApi: MastodonApi,
private val eventHub: EventHub private val eventHub: EventHub
) : TimelineCases { ) : TimelineCases {
/**
* Unused yet but can be use for cancellation later. It's always a good idea to save
* Disposables.
*/
private val cancelDisposable = CompositeDisposable()
override fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>) { override fun reblogWithCallback(status: Status, reblog: Boolean, callback: Callback<Status>) {
val id = status.actionableId val id = status.actionableId
@ -95,4 +105,13 @@ class TimelineCasesImpl(
eventHub.dispatch(StatusDeletedEvent(id)) eventHub.dispatch(StatusDeletedEvent(id))
} }
override fun pin(status: Status, pin: Boolean) {
// Replace with extension method if we use RxKotlin
(if (pin) mastodonApi.pinStatus(status.id) else mastodonApi.unpinStatus(status.id))
.subscribe({ updatedStatus ->
status.pinned = updatedStatus.pinned
}, {})
.addTo(this.cancelDisposable)
}
} }

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="pin" type="id" />
</resources>

View file

@ -353,5 +353,7 @@
<string name="profile_metadata_content_label">Content</string> <string name="profile_metadata_content_label">Content</string>
<string name="pref_title_absolute_time">Use absolute time</string> <string name="pref_title_absolute_time">Use absolute time</string>
<string name="unpin_action">Unpin</string>
<string name="pin_action">Pin</string>
</resources> </resources>

View file

@ -83,7 +83,8 @@ class BottomSheetActivityTest {
Status.Visibility.PUBLIC, Status.Visibility.PUBLIC,
listOf(), listOf(),
arrayOf(), arrayOf(),
null null,
pinned = false
) )
private val statusCallback = FakeSearchResults(status) private val statusCallback = FakeSearchResults(status)