move bottom sheet from fragments to activities (#628)
* move bottom sheet from fragments to activities * move BottomSheetLogic to dedicated abstract Activity * change tests * improve code
This commit is contained in:
parent
1c711eca22
commit
a2bfef3101
|
@ -75,7 +75,7 @@ import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public final class AccountActivity extends BaseActivity implements ActionButtonActivity,
|
public final class AccountActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||||
HasSupportFragmentInjector {
|
HasSupportFragmentInjector {
|
||||||
private static final String TAG = "AccountActivity"; // logging tag
|
private static final String TAG = "AccountActivity"; // logging tag
|
||||||
|
|
||||||
|
@ -329,8 +329,8 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewURL(String url) {
|
public void onViewUrl(String url) {
|
||||||
LinkHelper.openLink(url, note.getContext());
|
viewUrl(url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -711,4 +711,10 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return dispatchingAndroidInjector;
|
return dispatchingAndroidInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,10 +34,17 @@ import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
|
||||||
public abstract class BaseActivity extends AppCompatActivity {
|
public abstract class BaseActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
protected List<Call> callList;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AccountManager accountManager;
|
public AccountManager accountManager;
|
||||||
|
|
||||||
|
@ -75,6 +82,9 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||||
getTheme().applyStyle(style, false);
|
getTheme().applyStyle(style, false);
|
||||||
|
|
||||||
redirectIfNotLoggedIn();
|
redirectIfNotLoggedIn();
|
||||||
|
|
||||||
|
callList = new ArrayList<>();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -162,4 +172,12 @@ public abstract class BaseActivity extends AppCompatActivity {
|
||||||
.build()
|
.build()
|
||||||
.scheduleAsync();
|
.scheduleAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
for (Call call : callList) {
|
||||||
|
call.cancel();
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,196 @@
|
||||||
|
/* Copyright 2018 Conny Duck
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.annotation.VisibleForTesting
|
||||||
|
import android.support.design.widget.BottomSheetBehavior
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import com.keylesspalace.tusky.entity.SearchResults
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
|
import retrofit2.Call
|
||||||
|
import retrofit2.Callback
|
||||||
|
import retrofit2.Response
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.URISyntaxException
|
||||||
|
|
||||||
|
/** this is the base class for all activities that open links
|
||||||
|
* links are checked against the api if they are mastodon links so they can be openend in Tusky
|
||||||
|
* Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierachy
|
||||||
|
*/
|
||||||
|
|
||||||
|
abstract class BottomSheetActivity : BaseActivity() {
|
||||||
|
|
||||||
|
lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||||
|
var searchUrl: String? = null
|
||||||
|
|
||||||
|
abstract fun getMastodonApi(): MastodonApi
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun viewUrl(url: String) {
|
||||||
|
if (!looksLikeMastodonUrl(url)) {
|
||||||
|
openLink(url)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val call = getMastodonApi().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])
|
||||||
|
return
|
||||||
|
} else if (searchResult.accounts.isNotEmpty()) {
|
||||||
|
viewAccount(searchResult.accounts[0].id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
openLink(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<SearchResults>, t: Throwable) {
|
||||||
|
if (!getCancelSearchRequested(url)) {
|
||||||
|
onEndSearch(url)
|
||||||
|
openLink(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
callList.add(call)
|
||||||
|
onBeginSearch(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun viewThread(status: Status) {
|
||||||
|
if (!isSearching()) {
|
||||||
|
val intent = Intent(this, ViewThreadActivity::class.java)
|
||||||
|
intent.putExtra("id", status.actionableId)
|
||||||
|
intent.putExtra("url", status.actionableStatus.url)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun viewAccount(id: String) {
|
||||||
|
val intent = Intent(this, AccountActivity::class.java)
|
||||||
|
intent.putExtra("id", id)
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun onBeginSearch(url: String) {
|
||||||
|
searchUrl = url
|
||||||
|
showQuerySheet()
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun getCancelSearchRequested(url: String): Boolean {
|
||||||
|
return url != searchUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun isSearching(): Boolean {
|
||||||
|
return searchUrl != null
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun onEndSearch(url: String?) {
|
||||||
|
if (url == searchUrl) {
|
||||||
|
// Don't clear query if there's no match,
|
||||||
|
// since we might just now be getting the response for a canceled search
|
||||||
|
searchUrl = null
|
||||||
|
hideQuerySheet()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
fun cancelActiveSearch() {
|
||||||
|
if (isSearching()) {
|
||||||
|
onEndSearch(searchUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
open fun openLink(url: String) {
|
||||||
|
LinkHelper.openLink(url, this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showQuerySheet() {
|
||||||
|
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideQuerySheet() {
|
||||||
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://mastodon.foo.bar/@User
|
||||||
|
// https://mastodon.foo.bar/@User/43456787654678
|
||||||
|
// https://pleroma.foo.bar/users/User
|
||||||
|
// https://pleroma.foo.bar/users/43456787654678
|
||||||
|
// https://pleroma.foo.bar/notice/43456787654678
|
||||||
|
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
||||||
|
fun looksLikeMastodonUrl(urlString: String): Boolean {
|
||||||
|
val uri: URI
|
||||||
|
try {
|
||||||
|
uri = URI(urlString)
|
||||||
|
} catch (e: URISyntaxException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uri.query != null ||
|
||||||
|
uri.fragment != null ||
|
||||||
|
uri.path == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
val path = uri.path
|
||||||
|
return path.matches("^/@[^/]+$".toRegex()) ||
|
||||||
|
path.matches("^/users/[^/]+$".toRegex()) ||
|
||||||
|
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
|
||||||
|
path.matches("^/notice/\\d+$".toRegex()) ||
|
||||||
|
path.matches("^/objects/[-a-f0-9]+$".toRegex())
|
||||||
|
}
|
|
@ -24,6 +24,9 @@ import android.support.v7.widget.Toolbar;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -31,8 +34,10 @@ import dagger.android.AndroidInjector;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
public class FavouritesActivity extends BaseActivity implements HasSupportFragmentInjector {
|
public class FavouritesActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MastodonApi mastodonApi;
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||||
|
|
||||||
|
@ -71,4 +76,10 @@ public class FavouritesActivity extends BaseActivity implements HasSupportFragme
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return dispatchingAndroidInjector;
|
return dispatchingAndroidInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,8 @@ import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader;
|
||||||
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
|
import com.mikepenz.materialdrawer.util.DrawerImageLoader;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -72,7 +74,7 @@ import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class MainActivity extends BaseActivity implements ActionButtonActivity,
|
public class MainActivity extends BottomSheetActivity implements ActionButtonActivity,
|
||||||
HasSupportFragmentInjector {
|
HasSupportFragmentInjector {
|
||||||
private static final String TAG = "MainActivity"; // logging tag
|
private static final String TAG = "MainActivity"; // logging tag
|
||||||
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
|
private static final long DRAWER_ITEM_ADD_ACCOUNT = -13;
|
||||||
|
@ -550,4 +552,10 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity,
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return fragmentInjector;
|
return fragmentInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -10,13 +10,16 @@ import android.view.MenuItem
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment
|
import com.keylesspalace.tusky.fragment.TimelineFragment
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import dagger.android.AndroidInjector
|
import dagger.android.AndroidInjector
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.support.HasSupportFragmentInjector
|
import dagger.android.support.HasSupportFragmentInjector
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ModalTimelineActivity : BaseActivity(), ActionButtonActivity, HasSupportFragmentInjector {
|
class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var api: MastodonApi
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
|
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment>
|
||||||
|
|
||||||
|
@ -75,4 +78,9 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity, HasSupportFr
|
||||||
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
|
override fun supportFragmentInjector(): AndroidInjector<Fragment> {
|
||||||
return dispatchingAndroidInjector
|
return dispatchingAndroidInjector
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun getMastodonApi(): MastodonApi {
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,9 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.SearchFragment;
|
import com.keylesspalace.tusky.fragment.SearchFragment;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -37,9 +40,10 @@ import dagger.android.AndroidInjector;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
public class SearchActivity extends BaseActivity implements SearchView.OnQueryTextListener,
|
public class SearchActivity extends BottomSheetActivity implements SearchView.OnQueryTextListener,
|
||||||
HasSupportFragmentInjector {
|
HasSupportFragmentInjector {
|
||||||
|
@Inject
|
||||||
|
public MastodonApi mastodonApi;
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> fragmentInjector;
|
public DispatchingAndroidInjector<Fragment> fragmentInjector;
|
||||||
|
|
||||||
|
@ -139,4 +143,10 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return fragmentInjector;
|
return fragmentInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@ import android.support.v7.widget.Toolbar;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -31,8 +34,10 @@ import dagger.android.AndroidInjector;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
public class ViewTagActivity extends BaseActivity implements HasSupportFragmentInjector {
|
public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MastodonApi mastodonApi;
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||||
|
|
||||||
|
@ -74,4 +79,10 @@ public class ViewTagActivity extends BaseActivity implements HasSupportFragmentI
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return dispatchingAndroidInjector;
|
return dispatchingAndroidInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,15 +25,18 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.fragment.ViewThreadFragment;
|
import com.keylesspalace.tusky.fragment.ViewThreadFragment;
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.android.AndroidInjector;
|
import dagger.android.AndroidInjector;
|
||||||
import dagger.android.DispatchingAndroidInjector;
|
import dagger.android.DispatchingAndroidInjector;
|
||||||
import dagger.android.support.HasSupportFragmentInjector;
|
import dagger.android.support.HasSupportFragmentInjector;
|
||||||
|
|
||||||
public class ViewThreadActivity extends BaseActivity implements HasSupportFragmentInjector {
|
public class ViewThreadActivity extends BottomSheetActivity implements HasSupportFragmentInjector {
|
||||||
|
|
||||||
public static final int REVEAL_BUTTON_HIDDEN = 1;
|
public static final int REVEAL_BUTTON_HIDDEN = 1;
|
||||||
public static final int REVEAL_BUTTON_REVEAL = 2;
|
public static final int REVEAL_BUTTON_REVEAL = 2;
|
||||||
|
@ -41,6 +44,8 @@ public class ViewThreadActivity extends BaseActivity implements HasSupportFragme
|
||||||
|
|
||||||
private int revealButtonState = REVEAL_BUTTON_HIDDEN;
|
private int revealButtonState = REVEAL_BUTTON_HIDDEN;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public MastodonApi mastodonApi;
|
||||||
@Inject
|
@Inject
|
||||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector;
|
||||||
|
|
||||||
|
@ -113,4 +118,10 @@ public class ViewThreadActivity extends BaseActivity implements HasSupportFragme
|
||||||
public AndroidInjector<Fragment> supportFragmentInjector() {
|
public AndroidInjector<Fragment> supportFragmentInjector() {
|
||||||
return dispatchingAndroidInjector;
|
return dispatchingAndroidInjector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Override
|
||||||
|
public MastodonApi getMastodonApi() {
|
||||||
|
return mastodonApi;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,49 +20,35 @@ import android.content.ClipboardManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.BottomSheetBehavior;
|
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.widget.PopupMenu;
|
import android.support.v7.widget.PopupMenu;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.LinearLayout;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.AccountActivity;
|
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
import com.keylesspalace.tusky.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.ReportActivity;
|
import com.keylesspalace.tusky.ReportActivity;
|
||||||
import com.keylesspalace.tusky.TuskyApplication;
|
import com.keylesspalace.tusky.TuskyApplication;
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.ViewTagActivity;
|
import com.keylesspalace.tusky.ViewTagActivity;
|
||||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
|
||||||
import com.keylesspalace.tusky.ViewVideoActivity;
|
import com.keylesspalace.tusky.ViewVideoActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
import com.keylesspalace.tusky.entity.SearchResults;
|
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
import com.keylesspalace.tusky.interfaces.AdapterItemRemover;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import retrofit2.Call;
|
|
||||||
import retrofit2.Callback;
|
|
||||||
import retrofit2.Response;
|
|
||||||
|
|
||||||
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
|
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
|
||||||
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
|
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
|
||||||
* of that is complicated by how they're coupled with Status and Notification and the corresponding
|
* of that is complicated by how they're coupled with Status and Notification and the corresponding
|
||||||
|
@ -74,10 +60,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
|
|
||||||
protected String loggedInAccountId;
|
protected String loggedInAccountId;
|
||||||
protected String loggedInUsername;
|
protected String loggedInUsername;
|
||||||
protected String searchUrl;
|
|
||||||
|
|
||||||
protected abstract TimelineCases timelineCases();
|
protected abstract TimelineCases timelineCases();
|
||||||
protected BottomSheetBehavior bottomSheet;
|
|
||||||
|
private BottomSheetActivity bottomSheetActivity;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
protected MastodonApi mastodonApi;
|
protected MastodonApi mastodonApi;
|
||||||
|
@ -91,7 +77,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
loggedInAccountId = activeAccount.getAccountId();
|
loggedInAccountId = activeAccount.getAccountId();
|
||||||
loggedInUsername = activeAccount.getUsername();
|
loggedInUsername = activeAccount.getUsername();
|
||||||
}
|
}
|
||||||
setupBottomSheet(getView());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -100,9 +85,31 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
getActivity().overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttach(Context context) {
|
||||||
|
super.onAttach(context);
|
||||||
|
if(context instanceof BottomSheetActivity) {
|
||||||
|
bottomSheetActivity = (BottomSheetActivity)context;
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Fragment must be attached to a BottomSheetActivity!");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected void openReblog(@Nullable final Status status) {
|
protected void openReblog(@Nullable final Status status) {
|
||||||
if (status == null) return;
|
if (status == null) return;
|
||||||
viewAccount(status.getAccount().getId());
|
bottomSheetActivity.viewAccount(status.getAccount().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void viewThread(Status status) {
|
||||||
|
bottomSheetActivity.viewThread(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void viewAccount(String accountId) {
|
||||||
|
bottomSheetActivity.viewAccount(accountId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onViewUrl(String url) {
|
||||||
|
bottomSheetActivity.viewUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void reply(Status status) {
|
protected void reply(Status status) {
|
||||||
|
@ -229,27 +236,12 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void viewThread(Status status) {
|
|
||||||
if (!isSearching()) {
|
|
||||||
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
|
|
||||||
intent.putExtra("id", status.getActionableId());
|
|
||||||
intent.putExtra("url", status.getActionableStatus().getUrl());
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void viewTag(String tag) {
|
protected void viewTag(String tag) {
|
||||||
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
Intent intent = new Intent(getContext(), ViewTagActivity.class);
|
||||||
intent.putExtra("hashtag", tag);
|
intent.putExtra("hashtag", tag);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void viewAccount(String id) {
|
|
||||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
|
||||||
intent.putExtra("id", id);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
protected void openReportPage(String accountId, String accountUsername, String statusId,
|
||||||
Spanned statusContent) {
|
Spanned statusContent) {
|
||||||
Intent intent = new Intent(getContext(), ReportActivity.class);
|
Intent intent = new Intent(getContext(), ReportActivity.class);
|
||||||
|
@ -260,144 +252,5 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://mastodon.foo.bar/@User
|
|
||||||
// https://mastodon.foo.bar/@User/43456787654678
|
|
||||||
// https://pleroma.foo.bar/users/User
|
|
||||||
// https://pleroma.foo.bar/users/43456787654678
|
|
||||||
// https://pleroma.foo.bar/notice/43456787654678
|
|
||||||
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
|
|
||||||
static boolean looksLikeMastodonUrl(String urlString) {
|
|
||||||
URI uri;
|
|
||||||
try {
|
|
||||||
uri = new URI(urlString);
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getQuery() != null ||
|
|
||||||
uri.getFragment() != null ||
|
|
||||||
uri.getPath() == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String path = uri.getPath();
|
|
||||||
return path.matches("^/@[^/]+$") ||
|
|
||||||
path.matches("^/users/[^/]+$") ||
|
|
||||||
path.matches("^/@[^/]+/\\d+$") ||
|
|
||||||
path.matches("^/notice/\\d+$") ||
|
|
||||||
path.matches("^/objects/[-a-f0-9]+$");
|
|
||||||
}
|
|
||||||
|
|
||||||
void onBeginSearch(@NonNull String url) {
|
|
||||||
searchUrl = url;
|
|
||||||
showQuerySheet();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean getCancelSearchRequested(@NonNull String url) {
|
|
||||||
return !url.equals(searchUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isSearching() {
|
|
||||||
return searchUrl != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void onEndSearch(@NonNull String url) {
|
|
||||||
if (url.equals(searchUrl)) {
|
|
||||||
// Don't clear query if there's no match,
|
|
||||||
// since we might just now be getting the response for a canceled search
|
|
||||||
searchUrl = null;
|
|
||||||
hideQuerySheet();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void cancelActiveSearch()
|
|
||||||
{
|
|
||||||
if (isSearching()) {
|
|
||||||
onEndSearch(searchUrl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void openLink(@NonNull String url) {
|
|
||||||
LinkHelper.openLink(url, getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onViewURL(String url) {
|
|
||||||
if (!looksLikeMastodonUrl(url)) {
|
|
||||||
openLink(url);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Call<SearchResults> call = mastodonApi.search(url, true);
|
|
||||||
call.enqueue(new Callback<SearchResults>() {
|
|
||||||
@Override
|
|
||||||
public void onResponse(@NonNull Call<SearchResults> call, @NonNull Response<SearchResults> 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
|
|
||||||
List<Status> statuses = response.body().getStatuses();
|
|
||||||
List<Account> accounts = response.body().getAccounts();
|
|
||||||
if (statuses != null && !statuses.isEmpty()) {
|
|
||||||
viewThread(statuses.get(0));
|
|
||||||
return;
|
|
||||||
} else if (accounts != null && !accounts.isEmpty()) {
|
|
||||||
viewAccount(accounts.get(0).getId());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
openLink(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onFailure(@NonNull Call<SearchResults> call, @NonNull Throwable t) {
|
|
||||||
if (!getCancelSearchRequested(url)) {
|
|
||||||
onEndSearch(url);
|
|
||||||
openLink(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
callList.add(call);
|
|
||||||
onBeginSearch(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void setupBottomSheet(View view)
|
|
||||||
{
|
|
||||||
LinearLayout bottomSheetLayout = view.findViewById(R.id.item_status_bottom_sheet);
|
|
||||||
if (bottomSheetLayout != null) {
|
|
||||||
bottomSheet = BottomSheetBehavior.from(bottomSheetLayout);
|
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_HIDDEN);
|
|
||||||
bottomSheet.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
|
|
||||||
@Override
|
|
||||||
public void onStateChanged(@NonNull View bottomSheet, int newState) {
|
|
||||||
switch(newState) {
|
|
||||||
case BottomSheetBehavior.STATE_HIDDEN:
|
|
||||||
cancelActiveSearch();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showQuerySheet() {
|
|
||||||
if (bottomSheet != null)
|
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void hideQuerySheet() {
|
|
||||||
if (bottomSheet != null)
|
|
||||||
bottomSheet.setState(BottomSheetBehavior.STATE_HIDDEN);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,5 +18,5 @@ package com.keylesspalace.tusky.interfaces;
|
||||||
public interface LinkListener {
|
public interface LinkListener {
|
||||||
void onViewTag(String tag);
|
void onViewTag(String tag);
|
||||||
void onViewAccount(String id);
|
void onViewAccount(String id);
|
||||||
void onViewURL(String url);
|
void onViewUrl(String url);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,10 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.Browser;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
import android.support.customtabs.CustomTabsIntent;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextPaint;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.ClickableSpan;
|
import android.text.style.ClickableSpan;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
|
@ -109,7 +107,7 @@ public class LinkHelper {
|
||||||
customSpan = new CustomURLSpan(span.getURL()) {
|
customSpan = new CustomURLSpan(span.getURL()) {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View widget) {
|
public void onClick(View widget) {
|
||||||
listener.onViewURL(getURL());
|
listener.onViewUrl(getURL());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,4 +232,6 @@
|
||||||
android:contentDescription="@string/action_mention"
|
android:contentDescription="@string/action_mention"
|
||||||
app:srcCompat="@drawable/ic_create_24dp" />
|
app:srcCompat="@drawable/ic_create_24dp" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -17,4 +17,6 @@
|
||||||
|
|
||||||
<include layout="@layout/toolbar_shadow_shim" />
|
<include layout="@layout/toolbar_shadow_shim" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -90,4 +90,6 @@
|
||||||
app:layout_anchorGravity="bottom|end"
|
app:layout_anchorGravity="bottom|end"
|
||||||
app:srcCompat="@drawable/ic_create_24dp" />
|
app:srcCompat="@drawable/ic_create_24dp" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -1,27 +1,22 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/activity_view_thread"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="com.keylesspalace.tusky.ModalTimelineActivity">
|
tools:context="com.keylesspalace.tusky.ModalTimelineActivity">
|
||||||
|
|
||||||
<include layout="@layout/toolbar_basic" />
|
<include layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
<include
|
|
||||||
layout="@layout/toolbar_shadow_shim"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="4dp"
|
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/appbar" />
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/content_frame"
|
android:id="@+id/content_frame"
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="match_parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/toolbar_shadow_shim"
|
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||||
app:layout_constraintLeft_toLeftOf="parent"
|
|
||||||
app:layout_constraintRight_toRightOf="parent"
|
<include layout="@layout/toolbar_shadow_shim" />
|
||||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
|
||||||
</android.support.constraint.ConstraintLayout>
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -31,4 +31,6 @@
|
||||||
|
|
||||||
<include layout="@layout/toolbar_shadow_shim" />
|
<include layout="@layout/toolbar_shadow_shim" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -17,4 +17,6 @@
|
||||||
|
|
||||||
<include layout="@layout/toolbar_shadow_shim" />
|
<include layout="@layout/toolbar_shadow_shim" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -17,4 +17,6 @@
|
||||||
|
|
||||||
<include layout="@layout/toolbar_shadow_shim" />
|
<include layout="@layout/toolbar_shadow_shim" />
|
||||||
|
|
||||||
|
<include layout="@layout/item_status_bottom_sheet"/>
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
|
@ -26,6 +26,4 @@
|
||||||
android:text="@string/search_no_results"
|
android:text="@string/search_no_results"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
<include layout="@layout/item_status_bottom_sheet" />
|
|
||||||
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.design.widget.CoordinatorLayout
|
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_width="match_parent">
|
android:layout_gravity="top">
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/swipe_refresh_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="top">
|
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
<include layout="@layout/item_status_bottom_sheet"/>
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
|
|
@ -1,19 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<android.support.design.widget.CoordinatorLayout
|
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
android:id="@+id/swipe_refresh_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_width="match_parent">
|
android:layout_gravity="top">
|
||||||
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/swipe_refresh_layout"
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/recycler_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="top">
|
android:scrollbars="vertical" />
|
||||||
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
<android.support.v7.widget.RecyclerView
|
|
||||||
android:id="@+id/recycler_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:scrollbars="vertical" />
|
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
|
||||||
<include layout="@layout/item_status_bottom_sheet" />
|
|
||||||
</android.support.design.widget.CoordinatorLayout>
|
|
||||||
|
|
|
@ -13,14 +13,15 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
|
import android.support.design.widget.BottomSheetBehavior
|
||||||
import android.text.SpannedString
|
import android.text.SpannedString
|
||||||
|
import android.widget.LinearLayout
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.SearchResults
|
import com.keylesspalace.tusky.entity.SearchResults
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
|
@ -35,8 +36,8 @@ import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
class SFragmentTest {
|
class BottomSheetActivityTest {
|
||||||
private lateinit var fragment : FakeSFragment
|
private lateinit var activity : FakeBottomSheetActivity
|
||||||
private lateinit var apiMock: MastodonApi
|
private lateinit var apiMock: MastodonApi
|
||||||
private val accountQuery = "http://mastodon.foo.bar/@User"
|
private val accountQuery = "http://mastodon.foo.bar/@User"
|
||||||
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
private val statusQuery = "http://mastodon.foo.bar/@User/345678"
|
||||||
|
@ -85,13 +86,12 @@ class SFragmentTest {
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
fragment = FakeSFragment()
|
|
||||||
|
|
||||||
apiMock = Mockito.mock(MastodonApi::class.java)
|
apiMock = Mockito.mock(MastodonApi::class.java)
|
||||||
`when`(apiMock.search(eq(accountQuery), ArgumentMatchers.anyBoolean())).thenReturn(accountCallback)
|
`when`(apiMock.search(eq(accountQuery), ArgumentMatchers.anyBoolean())).thenReturn(accountCallback)
|
||||||
`when`(apiMock.search(eq(statusQuery), ArgumentMatchers.anyBoolean())).thenReturn(statusCallback)
|
`when`(apiMock.search(eq(statusQuery), ArgumentMatchers.anyBoolean())).thenReturn(statusCallback)
|
||||||
`when`(apiMock.search(eq(nonMastodonQuery), ArgumentMatchers.anyBoolean())).thenReturn(emptyCallback)
|
`when`(apiMock.search(eq(nonMastodonQuery), ArgumentMatchers.anyBoolean())).thenReturn(emptyCallback)
|
||||||
fragment.mastodonApi = apiMock
|
|
||||||
|
activity = FakeBottomSheetActivity(apiMock)
|
||||||
}
|
}
|
||||||
|
|
||||||
@RunWith(Parameterized::class)
|
@RunWith(Parameterized::class)
|
||||||
|
@ -131,22 +131,22 @@ class SFragmentTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun test() {
|
fun test() {
|
||||||
Assert.assertEquals(expectedResult, SFragment.looksLikeMastodonUrl(url))
|
Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
|
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
|
||||||
fragment.onBeginSearch("https://mastodon.foo.bar/@User")
|
activity.onBeginSearch("https://mastodon.foo.bar/@User")
|
||||||
Assert.assertTrue(fragment.isSearching)
|
Assert.assertTrue(activity.isSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() {
|
fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() {
|
||||||
val validUrl = "https://mastodon.foo.bar/@User"
|
val validUrl = "https://mastodon.foo.bar/@User"
|
||||||
fragment.onBeginSearch(validUrl)
|
activity.onBeginSearch(validUrl)
|
||||||
fragment.onEndSearch(validUrl)
|
activity.onEndSearch(validUrl)
|
||||||
Assert.assertFalse(fragment.isSearching)
|
Assert.assertFalse(activity.isSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -154,18 +154,18 @@ class SFragmentTest {
|
||||||
val validUrl = "https://mastodon.foo.bar/@User"
|
val validUrl = "https://mastodon.foo.bar/@User"
|
||||||
val invalidUrl = ""
|
val invalidUrl = ""
|
||||||
|
|
||||||
fragment.onBeginSearch(validUrl)
|
activity.onBeginSearch(validUrl)
|
||||||
fragment.onEndSearch(invalidUrl)
|
activity.onEndSearch(invalidUrl)
|
||||||
Assert.assertTrue(fragment.isSearching)
|
Assert.assertTrue(activity.isSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun cancelActiveSearch() {
|
fun cancelActiveSearch() {
|
||||||
val url = "https://mastodon.foo.bar/@User"
|
val url = "https://mastodon.foo.bar/@User"
|
||||||
|
|
||||||
fragment.onBeginSearch(url)
|
activity.onBeginSearch(url)
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
Assert.assertFalse(fragment.isSearching)
|
Assert.assertFalse(activity.isSearching())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -173,85 +173,84 @@ class SFragmentTest {
|
||||||
val firstUrl = "https://mastodon.foo.bar/@User"
|
val firstUrl = "https://mastodon.foo.bar/@User"
|
||||||
val secondUrl = "https://mastodon.foo.bar/@meh"
|
val secondUrl = "https://mastodon.foo.bar/@meh"
|
||||||
|
|
||||||
fragment.onBeginSearch(firstUrl)
|
activity.onBeginSearch(firstUrl)
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
|
|
||||||
fragment.onBeginSearch(secondUrl)
|
activity.onBeginSearch(secondUrl)
|
||||||
Assert.assertTrue(fragment.getCancelSearchRequested(firstUrl))
|
Assert.assertTrue(activity.getCancelSearchRequested(firstUrl))
|
||||||
Assert.assertFalse(fragment.getCancelSearchRequested(secondUrl))
|
Assert.assertFalse(activity.getCancelSearchRequested(secondUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
fun search_inIdealConditions_returnsRequestedResults_forAccount() {
|
||||||
fragment.onViewURL(accountQuery)
|
activity.viewUrl(accountQuery)
|
||||||
accountCallback.invokeCallback()
|
accountCallback.invokeCallback()
|
||||||
Assert.assertEquals(account.id, fragment.accountId)
|
Assert.assertEquals(account.id, activity.accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
fun search_inIdealConditions_returnsRequestedResults_forStatus() {
|
||||||
fragment.onViewURL(statusQuery)
|
activity.viewUrl(statusQuery)
|
||||||
statusCallback.invokeCallback()
|
statusCallback.invokeCallback()
|
||||||
Assert.assertEquals(status, fragment.status)
|
Assert.assertEquals(status, activity.status)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() {
|
||||||
fragment.onViewURL(nonMastodonQuery)
|
activity.viewUrl(nonMastodonQuery)
|
||||||
emptyCallback.invokeCallback()
|
emptyCallback.invokeCallback()
|
||||||
Assert.assertEquals(nonMastodonQuery, fragment.url)
|
Assert.assertEquals(nonMastodonQuery, activity.link)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_withCancellation_doesNotLoadUrl_forAccount() {
|
fun search_withCancellation_doesNotLoadUrl_forAccount() {
|
||||||
fragment.onViewURL(accountQuery)
|
activity.viewUrl(accountQuery)
|
||||||
Assert.assertTrue(fragment.isSearching)
|
Assert.assertTrue(activity.isSearching())
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
Assert.assertFalse(fragment.isSearching)
|
Assert.assertFalse(activity.isSearching())
|
||||||
accountCallback.invokeCallback()
|
accountCallback.invokeCallback()
|
||||||
Assert.assertEquals(null, fragment.accountId)
|
Assert.assertEquals(null, activity.accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
fun search_withCancellation_doesNotLoadUrl_forStatus() {
|
||||||
fragment.onViewURL(accountQuery)
|
activity.viewUrl(accountQuery)
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
accountCallback.invokeCallback()
|
accountCallback.invokeCallback()
|
||||||
Assert.assertEquals(null, fragment.accountId)
|
Assert.assertEquals(null, activity.accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() {
|
||||||
fragment.onViewURL(nonMastodonQuery)
|
activity.viewUrl(nonMastodonQuery)
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
emptyCallback.invokeCallback()
|
emptyCallback.invokeCallback()
|
||||||
Assert.assertEquals(null, fragment.url)
|
Assert.assertEquals(null, activity.searchUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun search_withPreviousCancellation_completes() {
|
fun search_withPreviousCancellation_completes() {
|
||||||
// begin/cancel account search
|
// begin/cancel account search
|
||||||
fragment.onViewURL(accountQuery)
|
activity.viewUrl(accountQuery)
|
||||||
fragment.cancelActiveSearch()
|
activity.cancelActiveSearch()
|
||||||
|
|
||||||
// begin status search
|
// begin status search
|
||||||
fragment.onViewURL(statusQuery)
|
activity.viewUrl(statusQuery)
|
||||||
|
|
||||||
// return response from account search
|
// return response from account search
|
||||||
accountCallback.invokeCallback()
|
accountCallback.invokeCallback()
|
||||||
|
|
||||||
// ensure that status search is still ongoing
|
// ensure that status search is still ongoing
|
||||||
Assert.assertTrue(fragment.isSearching)
|
Assert.assertTrue(activity.isSearching())
|
||||||
statusCallback.invokeCallback()
|
statusCallback.invokeCallback()
|
||||||
|
|
||||||
// ensure that the result of the status search was recorded
|
// ensure that the result of the status search was recorded
|
||||||
// and the account search wasn't
|
// and the account search wasn't
|
||||||
Assert.assertEquals(status, fragment.status)
|
Assert.assertEquals(status, activity.status)
|
||||||
Assert.assertEquals(null, fragment.accountId)
|
Assert.assertEquals(null, activity.accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeSearchResults : Call<SearchResults>
|
class FakeSearchResults : Call<SearchResults> {
|
||||||
{
|
|
||||||
private var searchResults: SearchResults
|
private var searchResults: SearchResults
|
||||||
private var callback: Callback<SearchResults>? = null
|
private var callback: Callback<SearchResults>? = null
|
||||||
|
|
||||||
|
@ -283,29 +282,33 @@ class SFragmentTest {
|
||||||
override fun request(): Request { throw NotImplementedError() }
|
override fun request(): Request { throw NotImplementedError() }
|
||||||
}
|
}
|
||||||
|
|
||||||
class FakeSFragment : SFragment() {
|
class FakeBottomSheetActivity(val api: MastodonApi) : BottomSheetActivity() {
|
||||||
|
|
||||||
var status: Status? = null
|
var status: Status? = null
|
||||||
var accountId: String? = null
|
var accountId: String? = null
|
||||||
var url: String? = null
|
var link: String? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
callList = mutableListOf()
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
bottomSheet = Mockito.mock(BottomSheetBehavior::class.java) as BottomSheetBehavior<LinearLayout>
|
||||||
|
callList = arrayListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getMastodonApi(): MastodonApi {
|
||||||
|
return api
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun openLink(url: String) {
|
override fun openLink(url: String) {
|
||||||
this.url = url
|
this.link = url
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun viewAccount(id: String?) {
|
override fun viewAccount(id: String) {
|
||||||
accountId = id
|
this.accountId = id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun viewThread(status: Status?) {
|
override fun viewThread(status: Status) {
|
||||||
this.status = status
|
this.status = status
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun removeItem(position: Int) { throw NotImplementedError() }
|
|
||||||
override fun removeAllByAccountId(accountId: String?) { throw NotImplementedError() }
|
|
||||||
override fun timelineCases(): TimelineCases { throw NotImplementedError() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue