diff --git a/app/build.gradle b/app/build.gradle index 4cd93818e..1282c7231 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,7 @@ android { } testOptions { unitTests { + returnDefaultValues = true includeAndroidResources = true } } diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt index b76bbab5d..3c2803365 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -17,16 +17,16 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Bundle -import androidx.annotation.VisibleForTesting -import com.google.android.material.bottomsheet.BottomSheetBehavior import android.view.View import android.widget.LinearLayout -import com.keylesspalace.tusky.entity.SearchResults +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.Lifecycle +import com.google.android.material.bottomsheet.BottomSheetBehavior 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.autoDisposable +import io.reactivex.android.schedulers.AndroidSchedulers import java.net.URI import java.net.URISyntaxException import javax.inject.Inject @@ -48,17 +48,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) {} + }) } @@ -68,41 +68,34 @@ abstract class BottomSheetActivity : BaseActivity() { return } - val call = mastodonApi.search(url, true) - call.enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - 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()) + .autoDisposable(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ (accounts, statuses) -> + if (getCancelSearchRequested(url)) { + return@subscribe } - } - openLink(url) - } - override fun onFailure(call: Call, 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) } @@ -159,11 +152,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 } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index 6abdf2cb1..a9ed60e47 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -80,7 +80,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; @@ -1827,71 +1827,67 @@ public final class ComposeActivity @Override public List search(String token) { - try { - switch (token.charAt(0)) { - case '@': - try { - List 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 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 results = - new ArrayList<>(); - List 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 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 results = + new ArrayList<>(); + List 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(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 02186b078..5adce8edb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -149,7 +149,7 @@ class FiltersActivity: BaseActivity() { addFilterButton.hide() filterProgressBar.show() - api.filters.enqueue(object : Callback> { + api.getFilters().enqueue(object : Callback> { override fun onResponse(call: Call>, response: Response>) { val filterResponse = response.body() if(response.isSuccessful && filterResponse != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 02e3d54ee..00f94ff9c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -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 com.keylesspalace.tusky.util.rickRoll import com.keylesspalace.tusky.util.shouldRickRoll import kotlinx.android.synthetic.main.activity_login.* @@ -222,14 +223,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 @@ -256,7 +257,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 diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java index 6dede94f7..f8018ba7e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java @@ -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(); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 864f2f260..3cb4745ad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -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> { override fun onFailure(call: Call>, t: Throwable) { // retrofit calls this on main thread so safe to call set value diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index 575667b63..625de5fe8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -113,7 +113,7 @@ 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)) .subscribe({ response -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index d50505a92..88b27285e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -61,7 +61,7 @@ class ReportViewModel @Inject constructor( private val selectedIds = HashSet() val statusViewState = StatusViewState() - var reportNote: String? = null + var reportNote: String = "" var isRemoteNotify = false private var statusId: String? = null diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt index 23ecc8f1a..10635dda1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt @@ -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 -> val ret = ArrayList() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 216aa2575..9b0be6544 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt index 9cb1895ae..d3da37484 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSource.kt @@ -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( private val disposables: CompositeDisposable, private val retryExecutor: Executor, private val initialItems: List? = null, - private val parser: (SearchResults2?) -> List) : PositionalDataSource() { + private val parser: (SearchResult?) -> List) : PositionalDataSource() { val networkState = MutableLiveData() @@ -56,7 +56,13 @@ class SearchDataSource( 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) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt index 2ea582953..7877c778b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchDataSourceFactory.kt @@ -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( private val disposables: CompositeDisposable, private val retryExecutor: Executor, private val cacheData: List? = null, - private val parser: (SearchResults2?) -> List) : DataSource.Factory() { + private val parser: (SearchResult?) -> List) : DataSource.Factory() { val sourceLiveData = MutableLiveData>() override fun create(): DataSource { val source = SearchDataSource(mastodonApi, searchType, searchRequest, disposables, retryExecutor, cacheData, parser) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt index 954c0315a..2cc12c20f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchRepository.kt @@ -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(private val mastodonApi: MastodonApi) { private val executor = Executors.newSingleThreadExecutor() fun getSearchData(searchType: SearchType, searchRequest: String?, disposables: CompositeDisposable, pageSize: Int = 20, - initialItems: List? = null, parser: (SearchResults2?) -> List): Listing { + initialItems: List? = null, parser: (SearchResult?) -> List): Listing { val sourceFactory = SearchDataSourceFactory(mastodonApi, searchType, searchRequest, disposables, executor, initialItems, parser) val livePagedList = sourceFactory.toLiveData( config = Config(pageSize = pageSize, enablePlaceholders = false, initialLoadSizeHint = pageSize * 2), diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults2.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt similarity index 96% rename from app/src/main/java/com/keylesspalace/tusky/entity/SearchResults2.kt rename to app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt index 7ad37a451..4307380ca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults2.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt @@ -15,7 +15,7 @@ package com.keylesspalace.tusky.entity -data class SearchResults2 ( +data class SearchResult ( val accounts: List, val statuses: List, val hashtags: List diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.kt deleted file mode 100644 index a8259910f..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResults.kt +++ /dev/null @@ -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 . */ - -package com.keylesspalace.tusky.entity - -data class SearchResults ( - val accounts: List, - val statuses: List, - val hashtags: List -) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index dcc7a7ba5..c4cf9993e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index 988dbc122..3415b3400 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -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> { override fun onFailure(call: Call>?, 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? { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java deleted file mode 100644 index 4d3a6d8a9..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ /dev/null @@ -1,428 +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 . */ - -package com.keylesspalace.tusky.network; - -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.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 androidx.annotation.Nullable; -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> homeTimeline( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); - - @GET("api/v1/timelines/home") - Single> homeTimelineSingle( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); - - @GET("api/v1/timelines/public") - Call> 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> 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> listTimeline( - @Path("listId") String listId, - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); - - @GET("api/v1/notifications") - Call> notifications( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit, - @Query("exclude_types[]") Set excludes); - - @GET("api/v1/notifications") - Call> notificationsWithAuth( - @Header("Authorization") String auth, @Header(DOMAIN_HEADER) String domain); - - @POST("api/v1/notifications/clear") - Call clearNotifications(); - - @GET("api/v1/notifications/{id}") - Call notification(@Path("id") String notificationId); - - @Multipart - @POST("api/v1/media") - Call uploadMedia(@Part MultipartBody.Part file); - - @FormUrlEncoded - @PUT("api/v1/media/{mediaId}") - Call updateMedia(@Path("mediaId") String mediaId, - @Field("description") String description); - - @POST("api/v1/statuses") - Call 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(@Path("id") String statusId); - - @GET("api/v1/statuses/{id}/context") - Call statusContext(@Path("id") String statusId); - - @GET("api/v1/statuses/{id}/reblogged_by") - Single>> statusRebloggedBy( - @Path("id") String statusId, - @Query("max_id") String maxId); - - @GET("api/v1/statuses/{id}/favourited_by") - Single>> statusFavouritedBy( - @Path("id") String statusId, - @Query("max_id") String maxId); - - @DELETE("api/v1/statuses/{id}") - Single deleteStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/reblog") - Single reblogStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/unreblog") - Single unreblogStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/favourite") - Single favouriteStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/unfavourite") - Single unfavouriteStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/pin") - Single pinStatus(@Path("id") String statusId); - - @POST("api/v1/statuses/{id}/unpin") - Single unpinStatus(@Path("id") String statusId); - - @GET("api/v1/accounts/verify_credentials") - Single accountVerifyCredentials(); - - @FormUrlEncoded - @PATCH("api/v1/accounts/update_credentials") - Call accountUpdateSource(@Nullable @Field("source[privacy]") String privacy, - @Nullable @Field("source[sensitive]") Boolean sensitive); - - @Multipart - @PATCH("api/v1/accounts/update_credentials") - Call 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> searchAccounts( - @Query("q") String q, - @Query("resolve") Boolean resolve, - @Query("limit") Integer limit, - @Query("following") Boolean following); - - @GET("api/v1/accounts/{id}") - Call 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> 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>> accountFollowers( - @Path("id") String accountId, - @Query("max_id") String maxId); - - @GET("api/v1/accounts/{id}/following") - Single>> accountFollowing( - @Path("id") String accountId, - @Query("max_id") String maxId); - - @FormUrlEncoded - @POST("api/v1/accounts/{id}/follow") - Call followAccount(@Path("id") String accountId, @Field("reblogs") boolean showReblogs); - - @POST("api/v1/accounts/{id}/unfollow") - Call unfollowAccount(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/block") - Call blockAccount(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/unblock") - Call unblockAccount(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/mute") - Call muteAccount(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/unmute") - Call unmuteAccount(@Path("id") String accountId); - - @GET("api/v1/accounts/relationships") - Call> relationships(@Query("id[]") List accountIds); - - @GET("api/v1/blocks") - Single>> blocks(@Query("max_id") String maxId); - - @GET("api/v1/mutes") - Single>> mutes(@Query("max_id") String maxId); - - @GET("api/v1/domain_blocks") - Single>> domainBlocks( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); - - @FormUrlEncoded - @POST("api/v1/domain_blocks") - Call blockDomain(@Field("domain") String domain); - - @FormUrlEncoded - // Normal @DELETE doesn't support fields? - @HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true) - Call unblockDomain(@Field("domain") String domain); - - @GET("api/v1/favourites") - Call> favourites( - @Query("max_id") String maxId, - @Query("since_id") String sinceId, - @Query("limit") Integer limit); - - @GET("api/v1/follow_requests") - Single>> followRequests(@Query("max_id") String maxId); - - @POST("api/v1/follow_requests/{id}/authorize") - Call authorizeFollowRequest(@Path("id") String accountId); - - @POST("api/v1/follow_requests/{id}/reject") - Call rejectFollowRequest(@Path("id") String accountId); - - @GET("api/v1/search") - Call search(@Query("q") String q, @Query("resolve") Boolean resolve); - - @FormUrlEncoded - @POST("api/v1/apps") - Call 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 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> getLists(); - - @FormUrlEncoded - @POST("api/v1/lists") - Single createList(@Field("title") String title); - - @FormUrlEncoded - @PUT("api/v1/lists/{listId}") - Single 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> 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 accountIds); - - @POST("api/v1/lists/{listId}/accounts") - Completable addCountToList(@Path("listId") String listId, - @Query("account_ids[]") List accountIds); - - @GET("/api/v1/custom_emojis") - Call> getCustomEmojis(); - - @GET("api/v1/instance") - Single getInstance(); - - @GET("/api/v1/conversations") - Call> getConversations(@Nullable @Query("max_id") String maxId, @Query("limit") int limit); - @GET("api/v1/filters") - Call> getFilters(); - - @FormUrlEncoded - @POST("api/v1/filters") - Call createFilter( - @Field("phrase") String phrase, - @Field("context[]") List context, - @Field("irreversible") Boolean irreversible, - @Field("whole_word") Boolean wholeWord, - @Field("expires_in") String expiresIn - ); - - @FormUrlEncoded - @PUT("api/v1/filters/{id}") - Call updateFilter( - @Path("id") String id, - @Field("phrase") String phrase, - @Field("context[]") List context, - @Field("irreversible") Boolean irreversible, - @Field("whole_word") Boolean wholeWord, - @Field("expires_in") String expiresIn - ); - - @DELETE("api/v1/filters/{id}") - Call deleteFilter( - @Path("id") String id - ); - - @FormUrlEncoded - @POST("api/v1/polls/{id}/votes") - Single voteInPoll( - @Path("id") String id, - @Field("choices[]") List choices - ); - - @POST("api/v1/accounts/{id}/block") - Single blockAccountObservable(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/unblock") - Single unblockAccountObservable(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/mute") - Single muteAccountObservable(@Path("id") String accountId); - - @POST("api/v1/accounts/{id}/unmute") - Single unmuteAccountObservable(@Path("id") String accountId); - - @GET("api/v1/accounts/relationships") - Single> relationshipsObservable(@Query("id[]") List accountIds); - - @FormUrlEncoded - @POST("api/v1/reports") - Single reportObservable( - @Field("account_id") String accountId, - @Field("status_ids[]") List statusIds, - @Field("comment") String comment, - @Field("forward") Boolean isNotifyRemote); - - @GET("api/v1/accounts/{id}/statuses") - Single> 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 statusObservable(@Path("id") String statusId); - - @GET("api/v2/search") - Single 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); - -} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt new file mode 100644 index 000000000..460b86e53 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -0,0 +1,519 @@ +/* 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 . */ + +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.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/ + */ + +@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> + + @GET("/api/v1/custom_emojis") + fun getCustomEmojis(): Call> + + @GET("api/v1/instance") + fun getInstance(): Single + + @GET("api/v1/filters") + fun getFilters(): Call> + + @GET("api/v1/timelines/home") + fun homeTimeline( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/timelines/home") + fun homeTimelineSingle( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Single> + + @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> + + @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> + + @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> + + @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? + ): Call> + + @GET("api/v1/notifications") + fun notificationsWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String + ): Call> + + @POST("api/v1/notifications/clear") + fun clearNotifications(): Call + + @GET("api/v1/notifications/{id}") + fun notification( + @Path("id") notificationId: String + ): Call + + @Multipart + @POST("api/v1/media") + fun uploadMedia( + @Part file: MultipartBody.Part + ): Call + + @FormUrlEncoded + @PUT("api/v1/media/{mediaId}") + fun updateMedia( + @Path("mediaId") mediaId: String, + @Field("description") description: String + ): Call + + @POST("api/v1/statuses") + fun createStatus( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body status: NewStatus + ): Call + + @GET("api/v1/statuses/{id}") + fun status( + @Path("id") statusId: String + ): Call + + @GET("api/v1/statuses/{id}/context") + fun statusContext( + @Path("id") statusId: String + ): Call + + @GET("api/v1/statuses/{id}/reblogged_by") + fun statusRebloggedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/statuses/{id}/favourited_by") + fun statusFavouritedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Single>> + + @DELETE("api/v1/statuses/{id}") + fun deleteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/reblog") + fun reblogStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unreblog") + fun unreblogStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/favourite") + fun favouriteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unfavourite") + fun unfavouriteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/pin") + fun pinStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unpin") + fun unpinStatus( + @Path("id") statusId: String + ): Single + + @GET("api/v1/accounts/verify_credentials") + fun accountVerifyCredentials(): Single + + @FormUrlEncoded + @PATCH("api/v1/accounts/update_credentials") + fun accountUpdateSource( + @Field("source[privacy]") privacy: String?, + @Field("source[sensitive]") sensitive: Boolean? + ): Call + + @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 + + @GET("api/v1/accounts/search") + fun searchAccounts( + @Query("q") q: String, + @Query("resolve") resolve: Boolean?, + @Query("limit") limit: Int?, + @Query("following") following: Boolean? + ): Single> + + @GET("api/v1/accounts/{id}") + fun account( + @Path("id") accountId: String + ): Call + + /** + * 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> + + @GET("api/v1/accounts/{id}/followers") + fun accountFollowers( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/accounts/{id}/following") + fun accountFollowing( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Single>> + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/follow") + fun followAccount( + @Path("id") accountId: String, + @Field("reblogs") showReblogs: Boolean + ): Call + + @POST("api/v1/accounts/{id}/unfollow") + fun unfollowAccount( + @Path("id") accountId: String + ): Call + + @POST("api/v1/accounts/{id}/block") + fun blockAccount( + @Path("id") accountId: String + ): Call + + @POST("api/v1/accounts/{id}/unblock") + fun unblockAccount( + @Path("id") accountId: String + ): Call + + @POST("api/v1/accounts/{id}/mute") + fun muteAccount( + @Path("id") accountId: String + ): Call + + @POST("api/v1/accounts/{id}/unmute") + fun unmuteAccount( + @Path("id") accountId: String + ): Call + + @GET("api/v1/accounts/relationships") + fun relationships( + @Query("id[]") accountIds: List + ): Call> + + @GET("api/v1/blocks") + fun blocks( + @Query("max_id") maxId: String? + ): Single>> + + @GET("api/v1/mutes") + fun mutes( + @Query("max_id") maxId: String? + ): Single>> + + @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>> + + @FormUrlEncoded + @POST("api/v1/domain_blocks") + fun blockDomain( + @Field("domain") domain: String + ): Call + + @FormUrlEncoded + // @DELETE doesn't support fields + @HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true) + fun unblockDomain(@Field("domain") domain: String): Call + + @GET("api/v1/favourites") + fun favourites( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Call> + + @GET("api/v1/follow_requests") + fun followRequests( + @Query("max_id") maxId: String? + ): Single>> + + @POST("api/v1/follow_requests/{id}/authorize") + fun authorizeFollowRequest( + @Path("id") accountId: String + ): Call + + @POST("api/v1/follow_requests/{id}/reject") + fun rejectFollowRequest( + @Path("id") accountId: String + ): Call + + @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 + + @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 + + @FormUrlEncoded + @POST("api/v1/lists") + fun createList( + @Field("title") title: String + ): Single + + @FormUrlEncoded + @PUT("api/v1/lists/{listId}") + fun updateList( + @Path("listId") listId: String, + @Field("title") title: String + ): Single + + @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> + + @DELETE("api/v1/lists/{listId}/accounts") + fun deleteAccountFromList( + @Path("listId") listId: String, + @Query("account_ids[]") accountIds: List + ): Completable + + @POST("api/v1/lists/{listId}/accounts") + fun addCountToList( + @Path("listId") listId: String, + @Query("account_ids[]") accountIds: List + ): Completable + + @GET("/api/v1/conversations") + fun getConversations( + @Query("max_id") maxId: String? = null, + @Query("limit") limit: Int + ): Call> + + @FormUrlEncoded + @POST("api/v1/filters") + fun createFilter( + @Field("phrase") phrase: String, + @Field("context[]") context: List, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresIn: String? + ): Call + + @FormUrlEncoded + @PUT("api/v1/filters/{id}") + fun updateFilter( + @Path("id") id: String, + @Field("phrase") phrase: String, + @Field("context[]") context: List, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresIn: String? + ): Call + + @DELETE("api/v1/filters/{id}") + fun deleteFilter( + @Path("id") id: String + ): Call + + @FormUrlEncoded + @POST("api/v1/polls/{id}/votes") + fun voteInPoll( + @Path("id") id: String, + @Field("choices[]") choices: List + ): Single + + @POST("api/v1/accounts/{id}/block") + fun blockAccountObservable( + @Path("id") accountId: String + ): Single + + @POST("api/v1/accounts/{id}/unblock") + fun unblockAccountObservable( + @Path("id") accountId: String + ): Single + + @POST("api/v1/accounts/{id}/mute") + fun muteAccountObservable( + @Path("id") accountId: String + ): Single + + @POST("api/v1/accounts/{id}/unmute") + fun unmuteAccountObservable( + @Path("id") accountId: String + ): Single + + @GET("api/v1/accounts/relationships") + fun relationshipsObservable( + @Query("id[]") accountIds: List + ): Single> + + @FormUrlEncoded + @POST("api/v1/reports") + fun reportObservable( + @Field("account_id") accountId: String, + @Field("status_ids[]") statusIds: List, + @Field("comment") comment: String, + @Field("forward") isNotifyRemote: Boolean? + ): Single + + @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> + + @GET("api/v1/statuses/{id}") + fun statusObservable( + @Path("id") statusId: String + ): Single + + @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 + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index 54078e477..24a73396e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -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) } } diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index f8833c64c..79d81a23d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -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 { - private var searchResults: SearchResults - private var callback: Callback? = 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?) { - this.callback = callback - } - - override fun isExecuted(): Boolean { throw NotImplementedError() } - override fun clone(): Call { throw NotImplementedError() } - override fun isCanceled(): Boolean { throw NotImplementedError() } - override fun cancel() { throw NotImplementedError() } - override fun execute(): Response { 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 + bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior callList = arrayListOf() } diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index e75147a69..343ac1e15 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -88,7 +88,7 @@ class ComposeActivityTest { accountManagerMock = Mockito.mock(AccountManager::class.java) apiMock = Mockito.mock(MastodonApi::class.java) - `when`(apiMock.customEmojis).thenReturn(object: Call> { + `when`(apiMock.getCustomEmojis()).thenReturn(object: Call> { override fun isExecuted(): Boolean { return false } @@ -110,7 +110,7 @@ class ComposeActivityTest { override fun enqueue(callback: Callback>?) {} }) - `when`(apiMock.instance).thenReturn(object: Single() { + `when`(apiMock.getInstance()).thenReturn(object: Single() { override fun subscribeActual(observer: SingleObserver) { val instance = instanceResponseCallback?.invoke() if (instance == null) {