Merge remote-tracking branch 'tuskyapp/master'
This commit is contained in:
commit
820a38b070
|
@ -66,6 +66,7 @@ android {
|
|||
}
|
||||
testOptions {
|
||||
unitTests {
|
||||
returnDefaultValues = true
|
||||
includeAndroidResources = true
|
||||
}
|
||||
}
|
||||
|
@ -119,10 +120,10 @@ dependencies {
|
|||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.0.1'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.0.1'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.1.0'
|
||||
implementation 'com.github.connyduck:sparkbutton:2.0.0'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.2.0'
|
||||
implementation 'com.squareup.okhttp3:logging-interceptor:4.2.0'
|
||||
implementation 'org.conscrypt:conscrypt-android:2.2.1'
|
||||
implementation 'com.github.connyduck:sparkbutton:2.0.1'
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.3.original@aar'
|
||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.8.0') {
|
||||
|
@ -148,8 +149,8 @@ dependencies {
|
|||
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
|
||||
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
|
||||
testImplementation 'org.robolectric:robolectric:4.3'
|
||||
testImplementation 'org.mockito:mockito-inline:2.28.2'
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0'
|
||||
testImplementation 'org.mockito:mockito-inline:3.0.0'
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.1', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
|
@ -157,17 +158,16 @@ dependencies {
|
|||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
testImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.10'
|
||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.12'
|
||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.3.0'
|
||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.3.0'
|
||||
implementation 'com.uber.autodispose:autodispose:1.3.0'
|
||||
implementation 'io.reactivex.rxjava2:rxkotlin:2.4.0'
|
||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.4.0'
|
||||
implementation 'com.uber.autodispose:autodispose:1.4.0'
|
||||
implementation 'androidx.paging:paging-runtime-ktx:2.1.0'
|
||||
|
||||
//Glide
|
||||
implementation 'com.github.bumptech.glide:glide:4.9.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.9.0'
|
||||
implementation 'jp.wasabeef:glide-transformations:3.1.1' // intentionally use 3.x version because of 2mb smaller apk
|
||||
implementation 'com.github.bumptech.glide:glide:4.10.0'
|
||||
implementation 'com.github.bumptech.glide:okhttp3-integration:4.10.0'
|
||||
|
||||
//Add some useful extensions
|
||||
implementation 'androidx.core:core-ktx:1.2.0-alpha01'
|
||||
|
|
|
@ -36,6 +36,7 @@ import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
|||
import com.keylesspalace.tusky.viewmodel.State
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import kotlinx.android.synthetic.main.fragment_accounts_in_list.*
|
||||
|
@ -106,7 +107,7 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this))
|
||||
.autoDispose(from(this))
|
||||
.subscribe { state ->
|
||||
adapter.submitList(state.accounts.asRightOrNull() ?: listOf())
|
||||
|
||||
|
|
|
@ -20,13 +20,13 @@ import android.os.Bundle
|
|||
import android.view.View
|
||||
import android.widget.LinearLayout
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import javax.inject.Inject
|
||||
|
@ -50,17 +50,17 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
super.onPostCreate(savedInstanceState)
|
||||
|
||||
val bottomSheetLayout: LinearLayout = findViewById(R.id.item_status_bottom_sheet)
|
||||
bottomSheet = BottomSheetBehavior.from(bottomSheetLayout)
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
cancelActiveSearch()
|
||||
}
|
||||
bottomSheet = BottomSheetBehavior.from(bottomSheetLayout)
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
bottomSheet.setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
if (newState == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
cancelActiveSearch()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
@ -75,41 +75,34 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
return
|
||||
}
|
||||
|
||||
val call = mastodonApi.search(url, true)
|
||||
call.enqueue(object : Callback<SearchResults> {
|
||||
override fun onResponse(call: Call<SearchResults>, response: Response<SearchResults>) {
|
||||
if (getCancelSearchRequested(url)) {
|
||||
return
|
||||
}
|
||||
|
||||
onEndSearch(url)
|
||||
if (response.isSuccessful) {
|
||||
// According to the mastodon API doc, if the search query is a url,
|
||||
// only exact matches for statuses or accounts are returned
|
||||
// which is good, because pleroma returns a different url
|
||||
// than the public post link
|
||||
val searchResult = response.body()
|
||||
if(searchResult != null) {
|
||||
if (searchResult.statuses.isNotEmpty()) {
|
||||
viewThread(searchResult.statuses[0].id, searchResult.statuses[0].url)
|
||||
return
|
||||
} else if (searchResult.accounts.isNotEmpty()) {
|
||||
viewAccount(searchResult.accounts[0].id)
|
||||
return
|
||||
}
|
||||
mastodonApi.searchObservable(
|
||||
query = url,
|
||||
resolve = true
|
||||
).observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ (accounts, statuses) ->
|
||||
if (getCancelSearchRequested(url)) {
|
||||
return@subscribe
|
||||
}
|
||||
}
|
||||
openLink(url)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<SearchResults>, t: Throwable) {
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
|
||||
if (statuses.isNotEmpty()) {
|
||||
viewThread(statuses[0].id, statuses[0].url)
|
||||
return@subscribe
|
||||
} else if (accounts.isNotEmpty()) {
|
||||
viewAccount(accounts[0].id)
|
||||
return@subscribe
|
||||
}
|
||||
|
||||
openLink(url)
|
||||
}
|
||||
}
|
||||
})
|
||||
callList.add(call)
|
||||
}, {
|
||||
if (!getCancelSearchRequested(url)) {
|
||||
onEndSearch(url)
|
||||
openLink(url)
|
||||
}
|
||||
})
|
||||
|
||||
onBeginSearch(url)
|
||||
}
|
||||
|
||||
|
@ -166,11 +159,11 @@ abstract class BottomSheetActivity : BaseActivity() {
|
|||
}
|
||||
|
||||
private fun showQuerySheet() {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
private fun hideQuerySheet() {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ import com.keylesspalace.tusky.entity.Attachment;
|
|||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.Instance;
|
||||
import com.keylesspalace.tusky.entity.NewPoll;
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.SearchResult;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.network.MastodonApi;
|
||||
import com.keylesspalace.tusky.network.ProgressRequestBody;
|
||||
|
@ -1979,71 +1979,67 @@ public final class ComposeActivity
|
|||
|
||||
@Override
|
||||
public List<ComposeAutoCompleteAdapter.AutocompleteResult> search(String token) {
|
||||
try {
|
||||
switch (token.charAt(0)) {
|
||||
case '@':
|
||||
try {
|
||||
List<Account> accountList = mastodonApi
|
||||
.searchAccounts(token.substring(1), false, 20, null)
|
||||
.blockingGet();
|
||||
return CollectionsKt.map(accountList,
|
||||
ComposeAutoCompleteAdapter.AccountResult::new);
|
||||
} catch (Throwable e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
case '#':
|
||||
Response<SearchResults> response = mastodonApi.search(token, false).execute();
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
return CollectionsKt.map(
|
||||
response.body().getHashtags(),
|
||||
ComposeAutoCompleteAdapter.HashtagResult::new
|
||||
);
|
||||
} else {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
case ':':
|
||||
try {
|
||||
emojiListRetrievalLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s was interrupted.", token));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (emojiList != null) {
|
||||
String incomplete = token.substring(1).toLowerCase();
|
||||
|
||||
List<ComposeAutoCompleteAdapter.AutocompleteResult> results =
|
||||
new ArrayList<>();
|
||||
List<ComposeAutoCompleteAdapter.AutocompleteResult> resultsInside =
|
||||
new ArrayList<>();
|
||||
|
||||
for (Emoji emoji : emojiList) {
|
||||
String shortcode = emoji.getShortcode().toLowerCase();
|
||||
|
||||
if (shortcode.startsWith(incomplete)) {
|
||||
results.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||
resultsInside.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||
}
|
||||
}
|
||||
|
||||
if (!results.isEmpty() && !resultsInside.isEmpty()) {
|
||||
// both lists have results. include a separator between them.
|
||||
results.add(new ComposeAutoCompleteAdapter.ResultSeparator());
|
||||
}
|
||||
|
||||
results.addAll(resultsInside);
|
||||
return results;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
default:
|
||||
Log.w(TAG, "Unexpected autocompletion token: " + token);
|
||||
switch (token.charAt(0)) {
|
||||
case '@':
|
||||
try {
|
||||
List<Account> accountList = mastodonApi
|
||||
.searchAccounts(token.substring(1), false, 20, null)
|
||||
.blockingGet();
|
||||
return CollectionsKt.map(accountList,
|
||||
ComposeAutoCompleteAdapter.AccountResult::new);
|
||||
} catch (Throwable e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
case '#':
|
||||
try {
|
||||
SearchResult searchResults = mastodonApi.searchObservable(token, null, false, null, null, null)
|
||||
.blockingGet();
|
||||
return CollectionsKt.map(
|
||||
searchResults.getHashtags(),
|
||||
ComposeAutoCompleteAdapter.HashtagResult::new
|
||||
);
|
||||
} catch (Throwable e) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
case ':':
|
||||
try {
|
||||
emojiListRetrievalLatch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Log.e(TAG, String.format("Autocomplete search for %s was interrupted.", token));
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (emojiList != null) {
|
||||
String incomplete = token.substring(1).toLowerCase();
|
||||
|
||||
List<ComposeAutoCompleteAdapter.AutocompleteResult> results =
|
||||
new ArrayList<>();
|
||||
List<ComposeAutoCompleteAdapter.AutocompleteResult> resultsInside =
|
||||
new ArrayList<>();
|
||||
|
||||
for (Emoji emoji : emojiList) {
|
||||
String shortcode = emoji.getShortcode().toLowerCase();
|
||||
|
||||
if (shortcode.startsWith(incomplete)) {
|
||||
results.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||
resultsInside.add(new ComposeAutoCompleteAdapter.EmojiResult(emoji));
|
||||
}
|
||||
}
|
||||
|
||||
if (!results.isEmpty() && !resultsInside.isEmpty()) {
|
||||
// both lists have results. include a separator between them.
|
||||
results.add(new ComposeAutoCompleteAdapter.ResultSeparator());
|
||||
}
|
||||
|
||||
results.addAll(resultsInside);
|
||||
return results;
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
default:
|
||||
Log.w(TAG, "Unexpected autocompletion token: " + token);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ class FiltersActivity: BaseActivity() {
|
|||
addFilterButton.hide()
|
||||
filterProgressBar.show()
|
||||
|
||||
api.filters.enqueue(object : Callback<List<Filter>> {
|
||||
api.getFilters().enqueue(object : Callback<List<Filter>> {
|
||||
override fun onResponse(call: Call<List<Filter>>, response: Response<List<Filter>>) {
|
||||
val filterResponse = response.body()
|
||||
if(response.isSuccessful && filterResponse != null) {
|
||||
|
|
|
@ -42,7 +42,7 @@ import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.*
|
|||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import dagger.android.DispatchingAndroidInjector
|
||||
import dagger.android.HasAndroidInjector
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
|
@ -92,7 +92,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
viewModel = viewModelFactory.create(ListsViewModel::class.java)
|
||||
viewModel.state
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this))
|
||||
.autoDispose(from(this))
|
||||
.subscribe(this::update)
|
||||
viewModel.retryLoading()
|
||||
|
||||
|
@ -101,7 +101,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector {
|
|||
}
|
||||
|
||||
viewModel.events.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this))
|
||||
.autoDispose(from(this))
|
||||
.subscribe { event ->
|
||||
@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA")
|
||||
when (event) {
|
||||
|
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.entity.AccessToken
|
|||
import com.keylesspalace.tusky.entity.AppCredentials
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import com.keylesspalace.tusky.util.getNonNullString
|
||||
import kotlinx.android.synthetic.main.activity_login.*
|
||||
import okhttp3.HttpUrl
|
||||
import retrofit2.Call
|
||||
|
@ -215,14 +216,14 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
val code = uri.getQueryParameter("code")
|
||||
val error = uri.getQueryParameter("error")
|
||||
|
||||
domain = preferences.getString(DOMAIN, "")!!
|
||||
/* During the redirect roundtrip this Activity usually dies, which wipes out the
|
||||
* instance variables, so they have to be recovered from where they were saved in
|
||||
* SharedPreferences. */
|
||||
domain = preferences.getNonNullString(DOMAIN, "")
|
||||
clientId = preferences.getString(CLIENT_ID, null)
|
||||
clientSecret = preferences.getString(CLIENT_SECRET, null)
|
||||
|
||||
if (code != null && domain.isNotEmpty()) {
|
||||
/* During the redirect roundtrip this Activity usually dies, which wipes out the
|
||||
* instance variables, so they have to be recovered from where they were saved in
|
||||
* SharedPreferences. */
|
||||
clientId = preferences.getString(CLIENT_ID, null)
|
||||
clientSecret = preferences.getString(CLIENT_SECRET, null)
|
||||
if (code != null && domain.isNotEmpty() && !clientId.isNullOrEmpty() && !clientSecret.isNullOrEmpty()) {
|
||||
|
||||
setLoading(true)
|
||||
/* Since authorization has succeeded, the final step to log in is to exchange
|
||||
|
@ -249,7 +250,7 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
}
|
||||
}
|
||||
|
||||
mastodonApi.fetchOAuthToken(domain, clientId, clientSecret, redirectUri, code,
|
||||
mastodonApi.fetchOAuthToken(domain, clientId!!, clientSecret!!, redirectUri, code,
|
||||
"authorization_code").enqueue(callback)
|
||||
} else if (error != null) {
|
||||
/* Authorization failed. Put the error response where the user can read it and they
|
||||
|
|
|
@ -32,7 +32,7 @@ import com.keylesspalace.tusky.di.Injectable
|
|||
import com.keylesspalace.tusky.util.onTextChanged
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.activity_tab_preference.*
|
||||
|
@ -252,7 +252,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
accountManager.saveAccount(it)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe()
|
||||
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.keylesspalace.tusky.di.AppInjector;
|
|||
import com.keylesspalace.tusky.util.EmojiCompatFont;
|
||||
import com.keylesspalace.tusky.util.LocaleManager;
|
||||
import com.keylesspalace.tusky.util.NotificationPullJobCreator;
|
||||
import com.uber.autodispose.AutoDisposePlugins;
|
||||
|
||||
import org.conscrypt.Conscrypt;
|
||||
|
||||
|
@ -86,6 +87,8 @@ public class TuskyApplication extends Application implements HasAndroidInjector
|
|||
}
|
||||
};
|
||||
|
||||
AutoDisposePlugins.setHideProxies(false);
|
||||
|
||||
initAppInjector();
|
||||
initEmojiCompat();
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ import com.keylesspalace.tusky.pager.ImagePagerAdapter
|
|||
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
|
||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
@ -285,7 +285,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
|||
.doOnDispose {
|
||||
futureTask.cancel(true)
|
||||
}
|
||||
.autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe(
|
||||
{ result ->
|
||||
Log.d(TAG, "Download image result: $result")
|
||||
|
|
|
@ -30,6 +30,7 @@ import com.bumptech.glide.Glide;
|
|||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Emoji;
|
||||
import com.keylesspalace.tusky.entity.HashTag;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
|
||||
|
@ -276,8 +277,8 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
|||
public final static class HashtagResult extends AutocompleteResult {
|
||||
private final String hashtag;
|
||||
|
||||
public HashtagResult(String hashtag) {
|
||||
this.hashtag = hashtag;
|
||||
public HashtagResult(HashTag hashtag) {
|
||||
this.hashtag = hashtag.getName();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||
import com.keylesspalace.tusky.entity.Card;
|
||||
|
@ -35,8 +36,6 @@ import java.util.Date;
|
|||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
|
||||
|
||||
class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
private TextView reblogs;
|
||||
private TextView favourites;
|
||||
|
@ -174,7 +173,13 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
if (!TextUtils.isEmpty(card.getImage())) {
|
||||
|
||||
RoundedCornersTransformation.CornerType cornertype;
|
||||
int topLeftRadius = 0;
|
||||
int topRightRadius = 0;
|
||||
int bottomRightRadius = 0;
|
||||
int bottomLeftRadius = 0;
|
||||
|
||||
int radius = cardImage.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.card_radius);
|
||||
|
||||
if (card.getWidth() > card.getHeight()) {
|
||||
cardView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
@ -184,7 +189,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
cornertype = RoundedCornersTransformation.CornerType.TOP;
|
||||
topLeftRadius = radius;
|
||||
topRightRadius = radius;
|
||||
} else {
|
||||
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
@ -192,15 +198,18 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
cornertype = RoundedCornersTransformation.CornerType.LEFT;
|
||||
topLeftRadius = radius;
|
||||
bottomLeftRadius = radius;
|
||||
}
|
||||
|
||||
int radius = cardImage.getContext().getResources()
|
||||
.getDimensionPixelSize(R.dimen.card_radius);
|
||||
|
||||
|
||||
Glide.with(cardImage)
|
||||
.load(card.getImage())
|
||||
.transform(new CenterCrop(), new RoundedCornersTransformation(radius, 0, cornertype))
|
||||
.transform(
|
||||
new CenterCrop(),
|
||||
new GranularRoundedCorners(topLeftRadius, topRightRadius, bottomRightRadius, bottomLeftRadius)
|
||||
)
|
||||
.into(cardImage);
|
||||
|
||||
} else {
|
||||
|
|
|
@ -36,7 +36,7 @@ class ConversationsRepository @Inject constructor(val mastodonApi: MastodonApi,
|
|||
networkState.value = NetworkState.LOADING
|
||||
}
|
||||
|
||||
mastodonApi.getConversations(null, DEFAULT_PAGE_SIZE).enqueue(
|
||||
mastodonApi.getConversations(limit = DEFAULT_PAGE_SIZE).enqueue(
|
||||
object : Callback<List<Conversation>> {
|
||||
override fun onFailure(call: Call<List<Conversation>>, t: Throwable) {
|
||||
// retrofit calls this on main thread so safe to call set value
|
||||
|
|
|
@ -21,7 +21,7 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_instance_list.*
|
||||
import retrofit2.Call
|
||||
|
@ -113,9 +113,9 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener {
|
|||
recyclerView.post { adapter.bottomLoading = true }
|
||||
}
|
||||
|
||||
api.domainBlocks(id, bottomId, null)
|
||||
api.domainBlocks(id, bottomId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ response ->
|
||||
val instances = response.body()
|
||||
|
||||
|
|
|
@ -61,7 +61,7 @@ class ReportViewModel @Inject constructor(
|
|||
private val selectedIds = HashSet<String>()
|
||||
val statusViewState = StatusViewState()
|
||||
|
||||
var reportNote: String? = null
|
||||
var reportNote: String = ""
|
||||
var isRemoteNotify = false
|
||||
|
||||
private var statusId: String? = null
|
||||
|
|
|
@ -72,10 +72,11 @@ class StatusesDataSource(private val accountId: String,
|
|||
retryBefore = null
|
||||
retryInitial = null
|
||||
initialLoad.postValue(NetworkState.LOADING)
|
||||
if (params.requestedInitialKey == null) {
|
||||
val initialKey = params.requestedInitialKey
|
||||
if (initialKey == null) {
|
||||
mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true)
|
||||
} else {
|
||||
mastodonApi.statusObservable(params.requestedInitialKey).zipWith(
|
||||
mastodonApi.statusObservable(initialKey).zipWith(
|
||||
mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true),
|
||||
BiFunction { status: Status, list: List<Status> ->
|
||||
val ret = ArrayList<Status>()
|
||||
|
|
|
@ -61,7 +61,7 @@ class ReportNoteFragment : Fragment(), Injectable {
|
|||
|
||||
private fun handleChanges() {
|
||||
editNote.doAfterTextChanged {
|
||||
viewModel.reportNote = it?.toString()
|
||||
viewModel.reportNote = it?.toString() ?: ""
|
||||
}
|
||||
checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked ->
|
||||
viewModel.isRemoteNotify = isChecked
|
||||
|
|
|
@ -19,7 +19,7 @@ import android.annotation.SuppressLint
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PositionalDataSource
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResults2
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
|
@ -32,7 +32,7 @@ class SearchDataSource<T>(
|
|||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val initialItems: List<T>? = null,
|
||||
private val parser: (SearchResults2?) -> List<T>) : PositionalDataSource<T>() {
|
||||
private val parser: (SearchResult?) -> List<T>) : PositionalDataSource<T>() {
|
||||
|
||||
val networkState = MutableLiveData<NetworkState>()
|
||||
|
||||
|
@ -56,7 +56,13 @@ class SearchDataSource<T>(
|
|||
networkState.postValue(NetworkState.LOADED)
|
||||
retry = null
|
||||
initialLoad.postValue(NetworkState.LOADING)
|
||||
mastodonApi.searchObservable(searchType.apiParameter, searchRequest, true, params.requestedLoadSize, 0, false)
|
||||
mastodonApi.searchObservable(
|
||||
query = searchRequest ?: "",
|
||||
type = searchType.apiParameter,
|
||||
resolve = true,
|
||||
limit = params.requestedLoadSize,
|
||||
offset = 0,
|
||||
following =false)
|
||||
.doOnSubscribe {
|
||||
disposables.add(it)
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResults2
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
import java.util.concurrent.Executor
|
||||
|
@ -30,7 +30,7 @@ class SearchDataSourceFactory<T>(
|
|||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val cacheData: List<T>? = null,
|
||||
private val parser: (SearchResults2?) -> List<T>) : DataSource.Factory<Int, T>() {
|
||||
private val parser: (SearchResult?) -> List<T>) : DataSource.Factory<Int, T>() {
|
||||
val sourceLiveData = MutableLiveData<SearchDataSource<T>>()
|
||||
override fun create(): DataSource<Int, T> {
|
||||
val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser)
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
import android.annotation.SuppressLint
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.PositionalDataSource
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
|
@ -17,7 +17,7 @@ class SearchNotestockDataSource(
|
|||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val initialItems: List<Pair<Status, StatusViewData.Concrete>>? = null,
|
||||
private val parser: (SearchResults?) -> List<Pair<Status, StatusViewData.Concrete>>) : PositionalDataSource<Pair<Status, StatusViewData.Concrete>>() {
|
||||
private val parser: (SearchResult?) -> List<Pair<Status, StatusViewData.Concrete>>) : PositionalDataSource<Pair<Status, StatusViewData.Concrete>>() {
|
||||
|
||||
val networkState = MutableLiveData<NetworkState>()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.paging.DataSource
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||
|
@ -15,7 +15,7 @@ class SearchNotestockDataSourceFactory(
|
|||
private val disposables: CompositeDisposable,
|
||||
private val retryExecutor: Executor,
|
||||
private val cacheData: List<Pair<Status, StatusViewData.Concrete>>? = null,
|
||||
private val parser: (SearchResults?) -> List<Pair<Status, StatusViewData.Concrete>>) : DataSource.Factory<Int, Pair<Status, StatusViewData.Concrete>>() {
|
||||
private val parser: (SearchResult?) -> List<Pair<Status, StatusViewData.Concrete>>) : DataSource.Factory<Int, Pair<Status, StatusViewData.Concrete>>() {
|
||||
val sourceLiveData = MutableLiveData<SearchNotestockDataSource>()
|
||||
override fun create(): DataSource<Int, Pair<Status, StatusViewData.Concrete>> {
|
||||
val source = SearchNotestockDataSource(notestockApi, searchRequest, disposables, retryExecutor, cacheData, parser)
|
||||
|
|
|
@ -3,7 +3,7 @@ package com.keylesspalace.tusky.components.search.adapter
|
|||
import androidx.lifecycle.Transformations
|
||||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.NotestockApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
|
@ -17,7 +17,7 @@ class SearchNotestockRepository(private val notestockApi: NotestockApi) {
|
|||
|
||||
fun getSearchData(searchRequest: String?, disposables: CompositeDisposable, pageSize: Int = 20,
|
||||
initialItems: List<Pair<Status, StatusViewData.Concrete>>? = null,
|
||||
parser: (SearchResults?) -> List<Pair<Status, StatusViewData.Concrete>>): Listing<Pair<Status, StatusViewData.Concrete>> {
|
||||
parser: (SearchResult?) -> List<Pair<Status, StatusViewData.Concrete>>): Listing<Pair<Status, StatusViewData.Concrete>> {
|
||||
val sourceFactory = SearchNotestockDataSourceFactory(notestockApi, searchRequest, disposables, executor, initialItems, parser)
|
||||
val livePagedList = sourceFactory.toLiveData(
|
||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
|
||||
|
|
|
@ -19,7 +19,7 @@ import androidx.lifecycle.Transformations
|
|||
import androidx.paging.Config
|
||||
import androidx.paging.toLiveData
|
||||
import com.keylesspalace.tusky.components.search.SearchType
|
||||
import com.keylesspalace.tusky.entity.SearchResults2
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Listing
|
||||
import io.reactivex.disposables.CompositeDisposable
|
||||
|
@ -30,7 +30,7 @@ class SearchRepository<T>(private val mastodonApi: MastodonApi) {
|
|||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
fun getSearchData(searchType: SearchType, searchRequest: String?, disposables: CompositeDisposable, pageSize: Int = 20,
|
||||
initialItems: List<T>? = null, parser: (SearchResults2?) -> List<T>): Listing<T> {
|
||||
initialItems: List<T>? = null, parser: (SearchResult?) -> List<T>): Listing<T> {
|
||||
val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser)
|
||||
val livePagedList = sourceFactory.toLiveData(
|
||||
config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2),
|
||||
|
|
|
@ -50,7 +50,7 @@ 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 com.uber.autodispose.autoDispose
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_search.*
|
||||
import java.util.*
|
||||
|
@ -420,7 +420,7 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
|||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
viewModel.deleteStatus(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe ({ deletedStatus ->
|
||||
removeItem(position)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
data class SearchResults2 (
|
||||
data class SearchResult (
|
||||
val accounts: List<Account>,
|
||||
val statuses: List<Status>,
|
||||
val hashtags: List<HashTag>
|
|
@ -1,22 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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
|
||||
|
||||
data class SearchResults (
|
||||
val accounts: List<Account>,
|
||||
val statuses: List<Status>,
|
||||
val hashtags: List<String>
|
||||
)
|
|
@ -40,7 +40,7 @@ import com.keylesspalace.tusky.util.hide
|
|||
import com.keylesspalace.tusky.util.show
|
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener
|
||||
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from
|
||||
import com.uber.autodispose.autoDisposable
|
||||
import com.uber.autodispose.autoDispose
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import kotlinx.android.synthetic.main.fragment_account_list.*
|
||||
|
@ -57,7 +57,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
lateinit var api: MastodonApi
|
||||
|
||||
private lateinit var type: Type
|
||||
private var id: String? = null
|
||||
private lateinit var id: String
|
||||
private lateinit var scrollListener: EndlessOnScrollListener
|
||||
private lateinit var adapter: AccountAdapter
|
||||
private var fetching = false
|
||||
|
@ -66,7 +66,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
type = arguments?.getSerializable(ARG_TYPE) as Type
|
||||
id = arguments?.getString(ARG_ID)
|
||||
id = arguments?.getString(ARG_ID)!!
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -275,7 +275,7 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable {
|
|||
|
||||
getFetchCallByListType(type, id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe({ response ->
|
||||
val accountList = response.body()
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
|
|||
private var fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
private var isVisibleToUser: Boolean = false
|
||||
|
||||
private var accountId: String?=null
|
||||
private lateinit var accountId: String
|
||||
|
||||
private val callback = object : Callback<List<Status>> {
|
||||
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
||||
|
@ -165,8 +165,8 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true)==true
|
||||
accountId = arguments?.getString(ACCOUNT_ID_ARG)
|
||||
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true
|
||||
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
||||
}
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
|
|
|
@ -83,6 +83,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
@ -1173,9 +1174,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) {
|
||||
if (oldItem.deepEquals(newItem)) {
|
||||
//If items are equal - update timestamp only
|
||||
List<String> payload = new ArrayList<>();
|
||||
payload.add(StatusBaseViewHolder.Key.KEY_CREATED);
|
||||
return payload;
|
||||
return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED);
|
||||
} else
|
||||
// If items are different - update a whole view holder
|
||||
return null;
|
||||
|
|
|
@ -1516,9 +1516,7 @@ public class TimelineFragment extends SFragment implements
|
|||
public Object getChangePayload(@NonNull StatusViewData oldItem, @NonNull StatusViewData newItem) {
|
||||
if (oldItem.deepEquals(newItem)) {
|
||||
//If items are equal - update timestamp only
|
||||
List<String> payload = new ArrayList<>();
|
||||
payload.add(StatusBaseViewHolder.Key.KEY_CREATED);
|
||||
return payload;
|
||||
return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED);
|
||||
} else
|
||||
// If items are different - update a whole view holder
|
||||
return null;
|
||||
|
|
|
@ -1,436 +0,0 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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.network;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.keylesspalace.tusky.entity.AccessToken;
|
||||
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;
|
||||
import com.keylesspalace.tusky.entity.MastoList;
|
||||
import com.keylesspalace.tusky.entity.NewStatus;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Poll;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus;
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.SearchResults2;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Single;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HTTP;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.PATCH;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
|
||||
/**
|
||||
* for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/
|
||||
*/
|
||||
public interface MastodonApi {
|
||||
String ENDPOINT_AUTHORIZE = "/oauth/authorize";
|
||||
String DOMAIN_HEADER = "domain";
|
||||
String PLACEHOLDER_DOMAIN = "dummy.placeholder";
|
||||
|
||||
@GET("api/v1/timelines/home")
|
||||
Call<List<Status>> homeTimeline(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/home")
|
||||
Single<List<Status>> homeTimelineSingle(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/public")
|
||||
Call<List<Status>> publicTimeline(
|
||||
@Query("local") Boolean local,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/tag/{hashtag}")
|
||||
Call<List<Status>> hashtagTimeline(
|
||||
@Path("hashtag") String hashtag,
|
||||
@Query("local") Boolean local,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/timelines/list/{listId}")
|
||||
Call<List<Status>> listTimeline(
|
||||
@Path("listId") String listId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
Call<List<Notification>> notifications(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit,
|
||||
@Query("exclude_types[]") Set<Notification.Type> excludes);
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
Call<List<Notification>> notificationsWithAuth(
|
||||
@Header("Authorization") String auth, @Header(DOMAIN_HEADER) String domain);
|
||||
|
||||
@POST("api/v1/notifications/clear")
|
||||
Call<ResponseBody> clearNotifications();
|
||||
|
||||
@GET("api/v1/notifications/{id}")
|
||||
Call<Notification> notification(@Path("id") String notificationId);
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/media")
|
||||
Call<Attachment> uploadMedia(@Part MultipartBody.Part file);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/media/{mediaId}")
|
||||
Call<Attachment> updateMedia(@Path("mediaId") String mediaId,
|
||||
@Field("description") String description);
|
||||
|
||||
@POST("api/v1/statuses")
|
||||
Call<Status> createStatus(
|
||||
@Header("Authorization") String auth,
|
||||
@Header(DOMAIN_HEADER) String domain,
|
||||
@Header("Idempotency-Key") String idempotencyKey,
|
||||
@Body NewStatus status);
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
Call<Status> status(@Path("id") String statusId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/context")
|
||||
Call<StatusContext> statusContext(@Path("id") String statusId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/reblogged_by")
|
||||
Single<Response<List<Account>>> statusRebloggedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/favourited_by")
|
||||
Single<Response<List<Account>>> statusFavouritedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@DELETE("api/v1/statuses/{id}")
|
||||
Single<DeletedStatus> deleteStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/reblog")
|
||||
Single<Status> reblogStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/unreblog")
|
||||
Single<Status> unreblogStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/favourite")
|
||||
Single<Status> favouriteStatus(@Path("id") String statusId);
|
||||
|
||||
@POST("api/v1/statuses/{id}/unfavourite")
|
||||
Single<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/scheduled_statuses")
|
||||
Call<List<ScheduledStatus>> scheduledStatuses();
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
Call<ResponseBody> deleteScheduledStatus(@Path("id") String scheduledStatusId);
|
||||
|
||||
@GET("api/v1/accounts/verify_credentials")
|
||||
Single<Account> accountVerifyCredentials();
|
||||
|
||||
@FormUrlEncoded
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
Call<Account> accountUpdateSource(@Nullable @Field("source[privacy]") String privacy,
|
||||
@Nullable @Field("source[sensitive]") Boolean sensitive);
|
||||
|
||||
@Multipart
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
Call<Account> accountUpdateCredentials(
|
||||
@Nullable @Part(value="display_name") RequestBody displayName,
|
||||
@Nullable @Part(value="note") RequestBody note,
|
||||
@Nullable @Part(value="locked") RequestBody locked,
|
||||
@Nullable @Part MultipartBody.Part avatar,
|
||||
@Nullable @Part MultipartBody.Part header,
|
||||
@Nullable @Part(value="fields_attributes[0][name]") RequestBody fieldName0,
|
||||
@Nullable @Part(value="fields_attributes[0][value]") RequestBody fieldValue0,
|
||||
@Nullable @Part(value="fields_attributes[1][name]") RequestBody fieldName1,
|
||||
@Nullable @Part(value="fields_attributes[1][value]") RequestBody fieldValue1,
|
||||
@Nullable @Part(value="fields_attributes[2][name]") RequestBody fieldName2,
|
||||
@Nullable @Part(value="fields_attributes[2][value]") RequestBody fieldValue2,
|
||||
@Nullable @Part(value="fields_attributes[3][name]") RequestBody fieldName3,
|
||||
@Nullable @Part(value="fields_attributes[3][value]") RequestBody fieldValue3);
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
Single<List<Account>> searchAccounts(
|
||||
@Query("q") String q,
|
||||
@Query("resolve") Boolean resolve,
|
||||
@Query("limit") Integer limit,
|
||||
@Query("following") Boolean following);
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
Call<Account> account(@Path("id") String accountId);
|
||||
|
||||
/**
|
||||
* Method to fetch statuses for the specified account.
|
||||
* @param accountId ID for account for which statuses will be requested
|
||||
* @param maxId Only statuses with ID less than maxID will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceID will be returned
|
||||
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
|
||||
* @param excludeReplies only return statuses that are no replies
|
||||
* @param onlyMedia only return statuses that have media attached
|
||||
*/
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
Call<List<Status>> accountStatuses(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit,
|
||||
@Nullable @Query("exclude_replies") Boolean excludeReplies,
|
||||
@Nullable @Query("only_media") Boolean onlyMedia,
|
||||
@Nullable @Query("pinned") Boolean pinned);
|
||||
|
||||
@GET("api/v1/accounts/{id}/followers")
|
||||
Single<Response<List<Account>>> accountFollowers(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/accounts/{id}/following")
|
||||
Single<Response<List<Account>>> accountFollowing(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
Call<Relationship> followAccount(@Path("id") String accountId, @Field("reblogs") boolean showReblogs);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
Call<Relationship> unfollowAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
Call<Relationship> blockAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
Call<Relationship> unblockAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
Call<Relationship> muteAccount(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
Call<Relationship> unmuteAccount(@Path("id") String accountId);
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
Call<List<Relationship>> relationships(@Query("id[]") List<String> accountIds);
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
Single<Response<List<Account>>> blocks(@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/mutes")
|
||||
Single<Response<List<Account>>> mutes(@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/domain_blocks")
|
||||
Single<Response<List<String>>> domainBlocks(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/domain_blocks")
|
||||
Call<Object> blockDomain(@Field("domain") String domain);
|
||||
|
||||
@FormUrlEncoded
|
||||
// Normal @DELETE doesn't support fields?
|
||||
@HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true)
|
||||
Call<Object> unblockDomain(@Field("domain") String domain);
|
||||
|
||||
@GET("api/v1/favourites")
|
||||
Call<List<Status>> favourites(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/follow_requests")
|
||||
Single<Response<List<Account>>> followRequests(@Query("max_id") String maxId);
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
Call<Relationship> authorizeFollowRequest(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/reject")
|
||||
Call<Relationship> rejectFollowRequest(@Path("id") String accountId);
|
||||
|
||||
@GET("api/v1/search")
|
||||
Call<SearchResults> search(@Query("q") String q, @Query("resolve") Boolean resolve);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/apps")
|
||||
Call<AppCredentials> authenticateApp(
|
||||
@Header(DOMAIN_HEADER) String domain,
|
||||
@Field("client_name") String clientName,
|
||||
@Field("redirect_uris") String redirectUris,
|
||||
@Field("scopes") String scopes,
|
||||
@Field("website") String website);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
Call<AccessToken> fetchOAuthToken(
|
||||
@Header(DOMAIN_HEADER) String domain,
|
||||
@Field("client_id") String clientId,
|
||||
@Field("client_secret") String clientSecret,
|
||||
@Field("redirect_uri") String redirectUri,
|
||||
@Field("code") String code,
|
||||
@Field("grant_type") String grantType
|
||||
);
|
||||
|
||||
@GET("/api/v1/lists")
|
||||
Single<List<MastoList>> getLists();
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
Single<MastoList> createList(@Field("title") String title);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/lists/{listId}")
|
||||
Single<MastoList> updateList(@Path("listId") String listId, @Field("title") String title);
|
||||
|
||||
@DELETE("api/v1/lists/{listId}")
|
||||
Completable deleteList(@Path("listId") String listId);
|
||||
|
||||
@GET("api/v1/lists/{listId}/accounts")
|
||||
Single<List<Account>> getAccountsInList(@Path("listId") String listId, @Query("limit") int limit);
|
||||
|
||||
@DELETE("api/v1/lists/{listId}/accounts")
|
||||
Completable deleteAccountFromList(@Path("listId") String listId,
|
||||
@Query("account_ids[]") List<String> accountIds);
|
||||
|
||||
@POST("api/v1/lists/{listId}/accounts")
|
||||
Completable addCountToList(@Path("listId") String listId,
|
||||
@Query("account_ids[]") List<String> accountIds);
|
||||
|
||||
@GET("/api/v1/custom_emojis")
|
||||
Call<List<Emoji>> getCustomEmojis();
|
||||
|
||||
@GET("api/v1/instance")
|
||||
Single<Instance> getInstance();
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
Call<List<Conversation>> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit);
|
||||
@GET("api/v1/filters")
|
||||
Call<List<Filter>> getFilters();
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/filters")
|
||||
Call<Filter> createFilter(
|
||||
@Field("phrase") String phrase,
|
||||
@Field("context[]") List<String> context,
|
||||
@Field("irreversible") Boolean irreversible,
|
||||
@Field("whole_word") Boolean wholeWord,
|
||||
@Field("expires_in") String expiresIn
|
||||
);
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/filters/{id}")
|
||||
Call<Filter> updateFilter(
|
||||
@Path("id") String id,
|
||||
@Field("phrase") String phrase,
|
||||
@Field("context[]") List<String> context,
|
||||
@Field("irreversible") Boolean irreversible,
|
||||
@Field("whole_word") Boolean wholeWord,
|
||||
@Field("expires_in") String expiresIn
|
||||
);
|
||||
|
||||
@DELETE("api/v1/filters/{id}")
|
||||
Call<ResponseBody> deleteFilter(
|
||||
@Path("id") String id
|
||||
);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/polls/{id}/votes")
|
||||
Single<Poll> voteInPoll(
|
||||
@Path("id") String id,
|
||||
@Field("choices[]") List<Integer> choices
|
||||
);
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
Single<Relationship> blockAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
Single<Relationship> unblockAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
Single<Relationship> muteAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
Single<Relationship> unmuteAccountObservable(@Path("id") String accountId);
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
Single<List<Relationship>> relationshipsObservable(@Query("id[]") List<String> accountIds);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
Single<ResponseBody> reportObservable(
|
||||
@Field("account_id") String accountId,
|
||||
@Field("status_ids[]") List<String> statusIds,
|
||||
@Field("comment") String comment,
|
||||
@Field("forward") Boolean isNotifyRemote);
|
||||
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
Single<List<Status>> accountStatusesObservable(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit,
|
||||
@Nullable @Query("exclude_reblogs") Boolean excludeReblogs);
|
||||
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
Single<Status> statusObservable(@Path("id") String statusId);
|
||||
|
||||
@GET("api/v2/search")
|
||||
Single<SearchResults2> searchObservable(@Query("type") String type, @Query("q") String q, @Query("resolve") Boolean resolve, @Query("limit") Integer limit, @Query("offset") Integer offset, @Query("following") Boolean following);
|
||||
|
||||
}
|
|
@ -0,0 +1,515 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* 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.network
|
||||
|
||||
import com.keylesspalace.tusky.entity.*
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Single
|
||||
import okhttp3.MultipartBody
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.*
|
||||
import retrofit2.http.Field
|
||||
|
||||
/**
|
||||
* for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/
|
||||
*/
|
||||
|
||||
@JvmSuppressWildcards
|
||||
interface MastodonApi {
|
||||
|
||||
companion object {
|
||||
const val ENDPOINT_AUTHORIZE = "/oauth/authorize"
|
||||
const val DOMAIN_HEADER = "domain"
|
||||
const val PLACEHOLDER_DOMAIN = "dummy.placeholder"
|
||||
}
|
||||
|
||||
@GET("/api/v1/lists")
|
||||
fun getLists(): Single<List<MastoList>>
|
||||
|
||||
@GET("/api/v1/custom_emojis")
|
||||
fun getCustomEmojis(): Call<List<Emoji>>
|
||||
|
||||
@GET("api/v1/instance")
|
||||
fun getInstance(): Single<Instance>
|
||||
|
||||
@GET("api/v1/filters")
|
||||
fun getFilters(): Call<List<Filter>>
|
||||
|
||||
@GET("api/v1/timelines/home")
|
||||
fun homeTimeline(
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/timelines/home")
|
||||
fun homeTimelineSingle(
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Single<List<Status>>
|
||||
|
||||
@GET("api/v1/timelines/public")
|
||||
fun publicTimeline(
|
||||
@Query("local") local: Boolean?,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/timelines/tag/{hashtag}")
|
||||
fun hashtagTimeline(
|
||||
@Path("hashtag") hashtag: String,
|
||||
@Query("local") local: Boolean?,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/timelines/list/{listId}")
|
||||
fun listTimeline(
|
||||
@Path("listId") listId: String,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
fun notifications(
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("exclude_types[]") excludes: Set<Notification.Type>?
|
||||
): Call<List<Notification>>
|
||||
|
||||
@GET("api/v1/notifications")
|
||||
fun notificationsWithAuth(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String
|
||||
): Call<List<Notification>>
|
||||
|
||||
@POST("api/v1/notifications/clear")
|
||||
fun clearNotifications(): Call<ResponseBody>
|
||||
|
||||
@GET("api/v1/notifications/{id}")
|
||||
fun notification(
|
||||
@Path("id") notificationId: String
|
||||
): Call<Notification>
|
||||
|
||||
@Multipart
|
||||
@POST("api/v1/media")
|
||||
fun uploadMedia(
|
||||
@Part file: MultipartBody.Part
|
||||
): Call<Attachment>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/media/{mediaId}")
|
||||
fun updateMedia(
|
||||
@Path("mediaId") mediaId: String,
|
||||
@Field("description") description: String
|
||||
): Call<Attachment>
|
||||
|
||||
@POST("api/v1/statuses")
|
||||
fun createStatus(
|
||||
@Header("Authorization") auth: String,
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Header("Idempotency-Key") idempotencyKey: String,
|
||||
@Body status: NewStatus
|
||||
): Call<Status>
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
fun status(
|
||||
@Path("id") statusId: String
|
||||
): Call<Status>
|
||||
|
||||
@GET("api/v1/statuses/{id}/context")
|
||||
fun statusContext(
|
||||
@Path("id") statusId: String
|
||||
): Call<StatusContext>
|
||||
|
||||
@GET("api/v1/statuses/{id}/reblogged_by")
|
||||
fun statusRebloggedBy(
|
||||
@Path("id") statusId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@GET("api/v1/statuses/{id}/favourited_by")
|
||||
fun statusFavouritedBy(
|
||||
@Path("id") statusId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@DELETE("api/v1/statuses/{id}")
|
||||
fun deleteStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<DeletedStatus>
|
||||
|
||||
@POST("api/v1/statuses/{id}/reblog")
|
||||
fun reblogStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/unreblog")
|
||||
fun unreblogStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/favourite")
|
||||
fun favouriteStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/unfavourite")
|
||||
fun unfavouriteStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/pin")
|
||||
fun pinStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@POST("api/v1/statuses/{id}/unpin")
|
||||
fun unpinStatus(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@GET("api/v1/scheduled_statuses")
|
||||
fun scheduledStatuses(): Call<List<ScheduledStatus>>
|
||||
|
||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||
fun deleteScheduledStatus(
|
||||
@Path("id") scheduledStatusId: String
|
||||
): Call<ResponseBody>
|
||||
|
||||
@GET("api/v1/accounts/verify_credentials")
|
||||
fun accountVerifyCredentials(): Single<Account>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
fun accountUpdateSource(
|
||||
@Field("source[privacy]") privacy: String?,
|
||||
@Field("source[sensitive]") sensitive: Boolean?
|
||||
): Call<Account>
|
||||
|
||||
@Multipart
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
fun accountUpdateCredentials(
|
||||
@Part(value = "display_name") displayName: RequestBody?,
|
||||
@Part(value = "note") note: RequestBody?,
|
||||
@Part(value = "locked") locked: RequestBody?,
|
||||
@Part avatar: MultipartBody.Part?,
|
||||
@Part header: MultipartBody.Part?,
|
||||
@Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?,
|
||||
@Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?,
|
||||
@Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?,
|
||||
@Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?,
|
||||
@Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?,
|
||||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||
): Call<Account>
|
||||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
@Query("q") q: String,
|
||||
@Query("resolve") resolve: Boolean?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("following") following: Boolean?
|
||||
): Single<List<Account>>
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
fun account(
|
||||
@Path("id") accountId: String
|
||||
): Call<Account>
|
||||
|
||||
/**
|
||||
* Method to fetch statuses for the specified account.
|
||||
* @param accountId ID for account for which statuses will be requested
|
||||
* @param maxId Only statuses with ID less than maxID will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceID will be returned
|
||||
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
|
||||
* @param excludeReplies only return statuses that are no replies
|
||||
* @param onlyMedia only return statuses that have media attached
|
||||
*/
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
fun accountStatuses(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("exclude_replies") excludeReplies: Boolean?,
|
||||
@Query("only_media") onlyMedia: Boolean?,
|
||||
@Query("pinned") pinned: Boolean?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/followers")
|
||||
fun accountFollowers(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/following")
|
||||
fun accountFollowing(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
fun followAccount(
|
||||
@Path("id") accountId: String,
|
||||
@Field("reblogs") showReblogs: Boolean
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unfollow")
|
||||
fun unfollowAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccount(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationships(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Call<List<Relationship>>
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
fun blocks(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@GET("api/v1/mutes")
|
||||
fun mutes(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@GET("api/v1/domain_blocks")
|
||||
fun domainBlocks(
|
||||
@Query("max_id") maxId: String? = null,
|
||||
@Query("since_id") sinceId: String? = null,
|
||||
@Query("limit") limit: Int? = null
|
||||
): Single<Response<List<String>>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/domain_blocks")
|
||||
fun blockDomain(
|
||||
@Field("domain") domain: String
|
||||
): Call<Any>
|
||||
|
||||
@FormUrlEncoded
|
||||
// @DELETE doesn't support fields
|
||||
@HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true)
|
||||
fun unblockDomain(@Field("domain") domain: String): Call<Any>
|
||||
|
||||
@GET("api/v1/favourites")
|
||||
fun favourites(
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?
|
||||
): Call<List<Status>>
|
||||
|
||||
@GET("api/v1/follow_requests")
|
||||
fun followRequests(
|
||||
@Query("max_id") maxId: String?
|
||||
): Single<Response<List<Account>>>
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
fun authorizeFollowRequest(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/reject")
|
||||
fun rejectFollowRequest(
|
||||
@Path("id") accountId: String
|
||||
): Call<Relationship>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/apps")
|
||||
fun authenticateApp(
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Field("client_name") clientName: String,
|
||||
@Field("redirect_uris") redirectUris: String,
|
||||
@Field("scopes") scopes: String,
|
||||
@Field("website") website: String
|
||||
): Call<AppCredentials>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("oauth/token")
|
||||
fun fetchOAuthToken(
|
||||
@Header(DOMAIN_HEADER) domain: String,
|
||||
@Field("client_id") clientId: String,
|
||||
@Field("client_secret") clientSecret: String,
|
||||
@Field("redirect_uri") redirectUri: String,
|
||||
@Field("code") code: String,
|
||||
@Field("grant_type") grantType: String
|
||||
): Call<AccessToken>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/lists")
|
||||
fun createList(
|
||||
@Field("title") title: String
|
||||
): Single<MastoList>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/lists/{listId}")
|
||||
fun updateList(
|
||||
@Path("listId") listId: String,
|
||||
@Field("title") title: String
|
||||
): Single<MastoList>
|
||||
|
||||
@DELETE("api/v1/lists/{listId}")
|
||||
fun deleteList(
|
||||
@Path("listId") listId: String
|
||||
): Completable
|
||||
|
||||
@GET("api/v1/lists/{listId}/accounts")
|
||||
fun getAccountsInList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("limit") limit: Int
|
||||
): Single<List<Account>>
|
||||
|
||||
@DELETE("api/v1/lists/{listId}/accounts")
|
||||
fun deleteAccountFromList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("account_ids[]") accountIds: List<String>
|
||||
): Completable
|
||||
|
||||
@POST("api/v1/lists/{listId}/accounts")
|
||||
fun addCountToList(
|
||||
@Path("listId") listId: String,
|
||||
@Query("account_ids[]") accountIds: List<String>
|
||||
): Completable
|
||||
|
||||
@GET("/api/v1/conversations")
|
||||
fun getConversations(
|
||||
@Query("max_id") maxId: String? = null,
|
||||
@Query("limit") limit: Int
|
||||
): Call<List<Conversation>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/filters")
|
||||
fun createFilter(
|
||||
@Field("phrase") phrase: String,
|
||||
@Field("context[]") context: List<String>,
|
||||
@Field("irreversible") irreversible: Boolean?,
|
||||
@Field("whole_word") wholeWord: Boolean?,
|
||||
@Field("expires_in") expiresIn: String?
|
||||
): Call<Filter>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/filters/{id}")
|
||||
fun updateFilter(
|
||||
@Path("id") id: String,
|
||||
@Field("phrase") phrase: String,
|
||||
@Field("context[]") context: List<String>,
|
||||
@Field("irreversible") irreversible: Boolean?,
|
||||
@Field("whole_word") wholeWord: Boolean?,
|
||||
@Field("expires_in") expiresIn: String?
|
||||
): Call<Filter>
|
||||
|
||||
@DELETE("api/v1/filters/{id}")
|
||||
fun deleteFilter(
|
||||
@Path("id") id: String
|
||||
): Call<ResponseBody>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/polls/{id}/votes")
|
||||
fun voteInPoll(
|
||||
@Path("id") id: String,
|
||||
@Field("choices[]") choices: List<Int>
|
||||
): Single<Poll>
|
||||
|
||||
@POST("api/v1/accounts/{id}/block")
|
||||
fun blockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unblock")
|
||||
fun unblockAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/mute")
|
||||
fun muteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@POST("api/v1/accounts/{id}/unmute")
|
||||
fun unmuteAccountObservable(
|
||||
@Path("id") accountId: String
|
||||
): Single<Relationship>
|
||||
|
||||
@GET("api/v1/accounts/relationships")
|
||||
fun relationshipsObservable(
|
||||
@Query("id[]") accountIds: List<String>
|
||||
): Single<List<Relationship>>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/reports")
|
||||
fun reportObservable(
|
||||
@Field("account_id") accountId: String,
|
||||
@Field("status_ids[]") statusIds: List<String>,
|
||||
@Field("comment") comment: String,
|
||||
@Field("forward") isNotifyRemote: Boolean?
|
||||
): Single<ResponseBody>
|
||||
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
fun accountStatusesObservable(
|
||||
@Path("id") accountId: String,
|
||||
@Query("max_id") maxId: String?,
|
||||
@Query("since_id") sinceId: String?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("exclude_reblogs") excludeReblogs: Boolean?
|
||||
): Single<List<Status>>
|
||||
|
||||
@GET("api/v1/statuses/{id}")
|
||||
fun statusObservable(
|
||||
@Path("id") statusId: String
|
||||
): Single<Status>
|
||||
|
||||
@GET("api/v2/search")
|
||||
fun searchObservable(
|
||||
@Query("q") query: String?,
|
||||
@Query("type") type: String? = null,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("offset") offset: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Single<SearchResult>
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.keylesspalace.tusky.network;
|
||||
|
||||
import com.keylesspalace.tusky.entity.SearchResults;
|
||||
import com.keylesspalace.tusky.entity.SearchResult;
|
||||
|
||||
import io.reactivex.Single;
|
||||
import retrofit2.http.GET;
|
||||
|
@ -9,6 +9,6 @@ import retrofit2.http.Query;
|
|||
public interface NotestockApi {
|
||||
|
||||
@GET("api/v1/search.json")
|
||||
Single<SearchResults> search(@Query("q") String q);
|
||||
Single<SearchResult> search(@Query("q") String q);
|
||||
|
||||
}
|
||||
|
|
|
@ -273,13 +273,13 @@ class EditProfileViewModel @Inject constructor(
|
|||
if(instanceData.value == null || instanceData.value is Error) {
|
||||
instanceData.postValue(Loading())
|
||||
|
||||
mastodonApi.instance.subscribe(
|
||||
{instance ->
|
||||
instanceData.postValue(Success(instance))
|
||||
},
|
||||
{
|
||||
instanceData.postValue(Error())
|
||||
})
|
||||
mastodonApi.getInstance().subscribe(
|
||||
{ instance ->
|
||||
instanceData.postValue(Success(instance))
|
||||
},
|
||||
{
|
||||
instanceData.postValue(Error())
|
||||
})
|
||||
.addTo(disposeables)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -376,4 +376,50 @@
|
|||
<string name="pref_title_thread_filter_keywords">会話</string>
|
||||
<string name="caption_notoemoji">Googleの現在の絵文字セットです</string>
|
||||
|
||||
<string name="action_add_poll">投票を追加</string>
|
||||
<string name="action_mentions">返信</string>
|
||||
<string name="title_mentions_dialog">返信</string>
|
||||
<string name="dialog_redraft_toot_warning">このトゥートを削除し、下書きに戻しますか?</string>
|
||||
<string name="filter_dialog_remove_button">削除</string>
|
||||
<string name="filter_dialog_update_button">更新</string>
|
||||
<string name="error_create_list">リストを作成できませんでした。</string>
|
||||
<string name="error_delete_list">リストを削除できませんでした。</string>
|
||||
<string name="action_create_list">リストの作成</string>
|
||||
<string name="action_delete_list">リストの削除</string>
|
||||
<string name="action_edit_list">リストの編集</string>
|
||||
<string name="hint_search_people_list">フォロワーを検索</string>
|
||||
<string name="action_add_to_list">リストにアカウントを追加</string>
|
||||
<string name="action_remove_from_list">リストからアカウントを削除</string>
|
||||
|
||||
<string name="poll_ended_voted">参加した投票の結果がでました</string>
|
||||
<string name="poll_ended_created">作成した投票の結果がでました</string>
|
||||
|
||||
<string name="create_poll_title">投票</string>
|
||||
<string name="poll_duration_5_min">5分</string>
|
||||
<string name="poll_duration_30_min">30分</string>
|
||||
<string name="poll_duration_1_hour">1時間</string>
|
||||
<string name="poll_duration_6_hours">6時間</string>
|
||||
<string name="poll_duration_1_day">1日</string>
|
||||
<string name="poll_duration_3_days">3日</string>
|
||||
<string name="poll_duration_7_days">7日</string>
|
||||
<string name="add_poll_choice">選択肢を追加</string>
|
||||
<string name="poll_allow_multiple_choices">複数選択可</string>
|
||||
<string name="edit_poll">編集</string>
|
||||
|
||||
<string name="pref_title_bot_overlay">ボットマークを表示</string>
|
||||
<string name="pref_title_animate_gif_avatars">GIFアバターを動かす</string>
|
||||
|
||||
<string name="pref_title_public_filter_keywords">公開タイムライン</string>
|
||||
<string name="description_status_cw">閲覧注意:%s</string>
|
||||
<string name="edit_hashtag_title">ハッシュタグの編集</string>
|
||||
<string name="hashtag">ハッシュタグ</string>
|
||||
<string name="filter_apply">適用</string>
|
||||
|
||||
<string name="poll_info_closed">投票終了</string>
|
||||
<string name="title_accounts">アカウント</string>
|
||||
<string name="failed_search">検索に失敗しました</string>
|
||||
|
||||
<string name="pref_title_show_notifications_filter">通知フィルターを表示</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -15,26 +15,27 @@
|
|||
|
||||
package com.keylesspalace.tusky
|
||||
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import android.text.SpannedString
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.SearchResults
|
||||
import com.keylesspalace.tusky.entity.SearchResult
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import okhttp3.Request
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.plugins.RxAndroidPlugins
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import io.reactivex.schedulers.TestScheduler
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.mockito.ArgumentMatchers
|
||||
import org.mockito.Mockito
|
||||
import org.mockito.Mockito.*
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class BottomSheetActivityTest {
|
||||
private lateinit var activity : FakeBottomSheetActivity
|
||||
|
@ -42,7 +43,8 @@ class BottomSheetActivityTest {
|
|||
private val accountQuery = "http://mastodon.foo.bar/@User"
|
||||
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
||||
private val nonMastodonQuery = "http://medium.com/@correspondent/345678"
|
||||
private val emptyCallback = FakeSearchResults()
|
||||
private val emptyCallback = Single.just(SearchResult(emptyList(), emptyList(), emptyList()))
|
||||
private val testScheduler = TestScheduler()
|
||||
|
||||
private val account = Account (
|
||||
"1",
|
||||
|
@ -62,7 +64,7 @@ class BottomSheetActivityTest {
|
|||
emptyList(),
|
||||
emptyList()
|
||||
)
|
||||
private val accountCallback = FakeSearchResults(account)
|
||||
private val accountSingle = Single.just(SearchResult(listOf(account), emptyList(), emptyList()))
|
||||
|
||||
private val status = Status(
|
||||
"1",
|
||||
|
@ -88,14 +90,18 @@ class BottomSheetActivityTest {
|
|||
poll = null,
|
||||
card = null
|
||||
)
|
||||
private val statusCallback = FakeSearchResults(status)
|
||||
private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList()))
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
apiMock = Mockito.mock(MastodonApi::class.java)
|
||||
`when`(apiMock.search(eq(accountQuery), ArgumentMatchers.anyBoolean())).thenReturn(accountCallback)
|
||||
`when`(apiMock.search(eq(statusQuery), ArgumentMatchers.anyBoolean())).thenReturn(statusCallback)
|
||||
`when`(apiMock.search(eq(nonMastodonQuery), ArgumentMatchers.anyBoolean())).thenReturn(emptyCallback)
|
||||
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
RxAndroidPlugins.setMainThreadSchedulerHandler { testScheduler }
|
||||
|
||||
apiMock = mock(MastodonApi::class.java)
|
||||
`when`(apiMock.searchObservable(eq(accountQuery), eq(null), ArgumentMatchers.anyBoolean(), eq(null), eq(null), eq(null))).thenReturn(accountSingle)
|
||||
`when`(apiMock.searchObservable(eq(statusQuery), eq(null), ArgumentMatchers.anyBoolean(), eq(null), eq(null), eq(null))).thenReturn(statusSingle)
|
||||
`when`(apiMock.searchObservable(eq(nonMastodonQuery), eq(null), ArgumentMatchers.anyBoolean(), eq(null), eq(null), eq(null))).thenReturn(emptyCallback)
|
||||
|
||||
activity = FakeBottomSheetActivity(apiMock)
|
||||
}
|
||||
|
@ -190,21 +196,21 @@ class BottomSheetActivityTest {
|
|||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
||||
activity.viewUrl(accountQuery)
|
||||
accountCallback.invokeCallback()
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
Assert.assertEquals(account.id, activity.accountId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||
activity.viewUrl(statusQuery)
|
||||
statusCallback.invokeCallback()
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
Assert.assertEquals(status.id, activity.statusId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
emptyCallback.invokeCallback()
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
Assert.assertEquals(nonMastodonQuery, activity.link)
|
||||
}
|
||||
|
||||
|
@ -214,7 +220,6 @@ class BottomSheetActivityTest {
|
|||
Assert.assertTrue(activity.isSearching())
|
||||
activity.cancelActiveSearch()
|
||||
Assert.assertFalse(activity.isSearching())
|
||||
accountCallback.invokeCallback()
|
||||
Assert.assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
|
@ -222,7 +227,6 @@ class BottomSheetActivityTest {
|
|||
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
||||
activity.viewUrl(accountQuery)
|
||||
activity.cancelActiveSearch()
|
||||
accountCallback.invokeCallback()
|
||||
Assert.assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
|
@ -230,7 +234,6 @@ class BottomSheetActivityTest {
|
|||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
||||
activity.viewUrl(nonMastodonQuery)
|
||||
activity.cancelActiveSearch()
|
||||
emptyCallback.invokeCallback()
|
||||
Assert.assertEquals(null, activity.searchUrl)
|
||||
}
|
||||
|
||||
|
@ -243,12 +246,11 @@ class BottomSheetActivityTest {
|
|||
// begin status search
|
||||
activity.viewUrl(statusQuery)
|
||||
|
||||
// return response from account search
|
||||
accountCallback.invokeCallback()
|
||||
|
||||
// ensure that status search is still ongoing
|
||||
// ensure that search is still ongoing
|
||||
Assert.assertTrue(activity.isSearching())
|
||||
statusCallback.invokeCallback()
|
||||
|
||||
// return searchResults
|
||||
testScheduler.advanceTimeBy(100, TimeUnit.MILLISECONDS)
|
||||
|
||||
// ensure that the result of the status search was recorded
|
||||
// and the account search wasn't
|
||||
|
@ -256,38 +258,6 @@ class BottomSheetActivityTest {
|
|||
Assert.assertEquals(null, activity.accountId)
|
||||
}
|
||||
|
||||
class FakeSearchResults : Call<SearchResults> {
|
||||
private var searchResults: SearchResults
|
||||
private var callback: Callback<SearchResults>? = null
|
||||
|
||||
constructor() {
|
||||
searchResults = SearchResults(Collections.emptyList(), Collections.emptyList(), Collections.emptyList())
|
||||
}
|
||||
|
||||
constructor(status: Status) {
|
||||
searchResults = SearchResults(Collections.emptyList(), listOf(status), Collections.emptyList())
|
||||
}
|
||||
|
||||
constructor(account: Account) {
|
||||
searchResults = SearchResults(listOf(account), Collections.emptyList(), Collections.emptyList())
|
||||
}
|
||||
|
||||
fun invokeCallback() {
|
||||
callback?.onResponse(this, Response.success(searchResults))
|
||||
}
|
||||
|
||||
override fun enqueue(callback: Callback<SearchResults>?) {
|
||||
this.callback = callback
|
||||
}
|
||||
|
||||
override fun isExecuted(): Boolean { throw NotImplementedError() }
|
||||
override fun clone(): Call<SearchResults> { throw NotImplementedError() }
|
||||
override fun isCanceled(): Boolean { throw NotImplementedError() }
|
||||
override fun cancel() { throw NotImplementedError() }
|
||||
override fun execute(): Response<SearchResults> { throw NotImplementedError() }
|
||||
override fun request(): Request { throw NotImplementedError() }
|
||||
}
|
||||
|
||||
class FakeBottomSheetActivity(api: MastodonApi) : BottomSheetActivity() {
|
||||
|
||||
var statusId: String? = null
|
||||
|
@ -297,7 +267,7 @@ class BottomSheetActivityTest {
|
|||
init {
|
||||
mastodonApi = api
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
bottomSheet = Mockito.mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
||||
bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
||||
callList = arrayListOf()
|
||||
}
|
||||
|
||||
|
|
|
@ -88,7 +88,7 @@ class ComposeActivityTest {
|
|||
accountManagerMock = Mockito.mock(AccountManager::class.java)
|
||||
|
||||
apiMock = Mockito.mock(MastodonApi::class.java)
|
||||
`when`(apiMock.customEmojis).thenReturn(object: Call<List<Emoji>> {
|
||||
`when`(apiMock.getCustomEmojis()).thenReturn(object: Call<List<Emoji>> {
|
||||
override fun isExecuted(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
@ -110,7 +110,7 @@ class ComposeActivityTest {
|
|||
|
||||
override fun enqueue(callback: Callback<List<Emoji>>?) {}
|
||||
})
|
||||
`when`(apiMock.instance).thenReturn(object: Single<Instance>() {
|
||||
`when`(apiMock.getInstance()).thenReturn(object: Single<Instance>() {
|
||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
||||
val instance = instanceResponseCallback?.invoke()
|
||||
if (instance == null) {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
複数アカウントで利用可能なMastodonクライアント
|
|
@ -0,0 +1 @@
|
|||
Tusky
|
|
@ -0,0 +1,9 @@
|
|||
Tusky v9.0
|
||||
|
||||
- 이제 Tusky에서 투표를 만들 수 있습니다.
|
||||
- 검색 기능이 개선되었습니다.
|
||||
- 이제 계정 설정에서 컨텐츠 경고(CW)를 항상 연 상태로 설정할 수 있습니다.
|
||||
- 네비게이션 바의 프로필 이미지가 사각형으로 바뀌었습니다.
|
||||
- 이제 툿이 없는 이용자를 신고할 수 있습니다.
|
||||
- 안드로이드 6 이상에서 일반 텍스트를 통한 연결을 거부하기 시작합니다.
|
||||
- 그 외 자잘한 기능 개선과 버그를 해결하였습니다
|
Loading…
Reference in New Issue