Add ability to pin/unpin statuses (#872)
This commit is contained in:
parent
f6934cadd8
commit
a0988dc6c6
9 changed files with 61 additions and 13 deletions
|
@ -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'
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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);
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
4
app/src/main/res/values/ids.xml
Normal file
4
app/src/main/res/values/ids.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<item name="pin" type="id" />
|
||||||
|
</resources>
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue