diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index b1be2ef03..ff648cc4d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -75,7 +75,7 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -public final class AccountActivity extends BaseActivity implements ActionButtonActivity, +public final class AccountActivity extends BottomSheetActivity implements ActionButtonActivity, HasSupportFragmentInjector { private static final String TAG = "AccountActivity"; // logging tag @@ -329,8 +329,8 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA } @Override - public void onViewURL(String url) { - LinkHelper.openLink(url, note.getContext()); + public void onViewUrl(String url) { + viewUrl(url); } }); @@ -711,4 +711,10 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA public AndroidInjector supportFragmentInjector() { return dispatchingAndroidInjector; } + + @NonNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index f624a59cf..67a4960ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -34,10 +34,17 @@ import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.util.ThemeUtils; +import java.util.ArrayList; +import java.util.List; + import javax.inject.Inject; +import retrofit2.Call; + public abstract class BaseActivity extends AppCompatActivity { + protected List callList; + @Inject public AccountManager accountManager; @@ -75,6 +82,9 @@ public abstract class BaseActivity extends AppCompatActivity { getTheme().applyStyle(style, false); redirectIfNotLoggedIn(); + + callList = new ArrayList<>(); + } @Override @@ -162,4 +172,12 @@ public abstract class BaseActivity extends AppCompatActivity { .build() .scheduleAsync(); } + + @Override + protected void onDestroy() { + for (Call call : callList) { + call.cancel(); + } + super.onDestroy(); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt new file mode 100644 index 000000000..6fa36e746 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -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 . */ + +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 + 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 { + 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]) + return + } else if (searchResult.accounts.isNotEmpty()) { + viewAccount(searchResult.accounts[0].id) + return + } + } + } + openLink(url) + } + + override fun onFailure(call: Call, 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()) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java index 5dcbe2cb3..0d885839a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/FavouritesActivity.java @@ -24,6 +24,9 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import com.keylesspalace.tusky.fragment.TimelineFragment; +import com.keylesspalace.tusky.network.MastodonApi; + +import org.jetbrains.annotations.NotNull; import javax.inject.Inject; @@ -31,8 +34,10 @@ import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.support.HasSupportFragmentInjector; -public class FavouritesActivity extends BaseActivity implements HasSupportFragmentInjector { +public class FavouritesActivity extends BottomSheetActivity implements HasSupportFragmentInjector { + @Inject + public MastodonApi mastodonApi; @Inject public DispatchingAndroidInjector dispatchingAndroidInjector; @@ -71,4 +76,10 @@ public class FavouritesActivity extends BaseActivity implements HasSupportFragme public AndroidInjector supportFragmentInjector() { return dispatchingAndroidInjector; } + + @NotNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index d41f23664..0674457a2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -60,6 +60,8 @@ import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader; import com.mikepenz.materialdrawer.util.DrawerImageLoader; import com.squareup.picasso.Picasso; +import org.jetbrains.annotations.NotNull; + import java.util.ArrayList; import java.util.List; @@ -72,7 +74,7 @@ import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; -public class MainActivity extends BaseActivity implements ActionButtonActivity, +public class MainActivity extends BottomSheetActivity implements ActionButtonActivity, HasSupportFragmentInjector { private static final String TAG = "MainActivity"; // logging tag private static final long DRAWER_ITEM_ADD_ACCOUNT = -13; @@ -550,4 +552,10 @@ public class MainActivity extends BaseActivity implements ActionButtonActivity, public AndroidInjector supportFragmentInjector() { return fragmentInjector; } + + @NotNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index 42abea522..13caf1ce0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -10,13 +10,16 @@ import android.view.MenuItem import android.widget.FrameLayout import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.network.MastodonApi import dagger.android.AndroidInjector import dagger.android.DispatchingAndroidInjector import dagger.android.support.HasSupportFragmentInjector import javax.inject.Inject -class ModalTimelineActivity : BaseActivity(), ActionButtonActivity, HasSupportFragmentInjector { +class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportFragmentInjector { + @Inject + lateinit var api: MastodonApi @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector @@ -75,4 +78,9 @@ class ModalTimelineActivity : BaseActivity(), ActionButtonActivity, HasSupportFr override fun supportFragmentInjector(): AndroidInjector { return dispatchingAndroidInjector } + + override fun getMastodonApi(): MastodonApi { + return api + } + } diff --git a/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java index 430c8c5de..4c1566a78 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java @@ -30,6 +30,9 @@ import android.view.Menu; import android.view.MenuItem; import com.keylesspalace.tusky.fragment.SearchFragment; +import com.keylesspalace.tusky.network.MastodonApi; + +import org.jetbrains.annotations.NotNull; import javax.inject.Inject; @@ -37,9 +40,10 @@ import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.support.HasSupportFragmentInjector; -public class SearchActivity extends BaseActivity implements SearchView.OnQueryTextListener, +public class SearchActivity extends BottomSheetActivity implements SearchView.OnQueryTextListener, HasSupportFragmentInjector { - + @Inject + public MastodonApi mastodonApi; @Inject public DispatchingAndroidInjector fragmentInjector; @@ -139,4 +143,10 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe public AndroidInjector supportFragmentInjector() { return fragmentInjector; } + + @NotNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index 4d5b8fcfd..f3c9b7544 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -24,6 +24,9 @@ import android.support.v7.widget.Toolbar; import android.view.MenuItem; import com.keylesspalace.tusky.fragment.TimelineFragment; +import com.keylesspalace.tusky.network.MastodonApi; + +import org.jetbrains.annotations.NotNull; import javax.inject.Inject; @@ -31,8 +34,10 @@ import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; import dagger.android.support.HasSupportFragmentInjector; -public class ViewTagActivity extends BaseActivity implements HasSupportFragmentInjector { +public class ViewTagActivity extends BottomSheetActivity implements HasSupportFragmentInjector { + @Inject + public MastodonApi mastodonApi; @Inject public DispatchingAndroidInjector dispatchingAndroidInjector; @@ -74,4 +79,10 @@ public class ViewTagActivity extends BaseActivity implements HasSupportFragmentI public AndroidInjector supportFragmentInjector() { return dispatchingAndroidInjector; } + + @NotNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index 544b2833f..ad62f4f5b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -25,15 +25,18 @@ import android.view.Menu; import android.view.MenuItem; import com.keylesspalace.tusky.fragment.ViewThreadFragment; +import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.util.LinkHelper; +import org.jetbrains.annotations.NotNull; + import javax.inject.Inject; import dagger.android.AndroidInjector; import dagger.android.DispatchingAndroidInjector; 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_REVEAL = 2; @@ -41,6 +44,8 @@ public class ViewThreadActivity extends BaseActivity implements HasSupportFragme private int revealButtonState = REVEAL_BUTTON_HIDDEN; + @Inject + public MastodonApi mastodonApi; @Inject public DispatchingAndroidInjector dispatchingAndroidInjector; @@ -113,4 +118,10 @@ public class ViewThreadActivity extends BaseActivity implements HasSupportFragme public AndroidInjector supportFragmentInjector() { return dispatchingAndroidInjector; } + + @NotNull + @Override + public MastodonApi getMastodonApi() { + return mastodonApi; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index f083184f2..c3ad63de8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -20,49 +20,35 @@ import android.content.ClipboardManager; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import android.support.design.widget.BottomSheetBehavior; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.PopupMenu; import android.text.Spanned; 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.R; import com.keylesspalace.tusky.ReportActivity; import com.keylesspalace.tusky.TuskyApplication; import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewTagActivity; -import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.ViewVideoActivity; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; -import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Attachment; -import com.keylesspalace.tusky.entity.SearchResults; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.AdapterItemRemover; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; 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.List; import java.util.Set; 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 * 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 @@ -74,10 +60,10 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov protected String loggedInAccountId; protected String loggedInUsername; - protected String searchUrl; protected abstract TimelineCases timelineCases(); - protected BottomSheetBehavior bottomSheet; + + private BottomSheetActivity bottomSheetActivity; @Inject protected MastodonApi mastodonApi; @@ -91,7 +77,6 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov loggedInAccountId = activeAccount.getAccountId(); loggedInUsername = activeAccount.getUsername(); } - setupBottomSheet(getView()); } @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); } + @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) { 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) { @@ -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) { Intent intent = new Intent(getContext(), ViewTagActivity.class); intent.putExtra("hashtag", tag); 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, Spanned statusContent) { Intent intent = new Intent(getContext(), ReportActivity.class); @@ -260,144 +252,5 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov 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 call = mastodonApi.search(url, true); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull 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 - List statuses = response.body().getStatuses(); - List 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 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); - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java index cb0018690..90599b22f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.java @@ -18,5 +18,5 @@ package com.keylesspalace.tusky.interfaces; public interface LinkListener { void onViewTag(String tag); void onViewAccount(String id); - void onViewURL(String url); + void onViewUrl(String url); } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 75381e9a4..e529bc251 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -20,12 +20,10 @@ import android.content.Context; import android.content.Intent; import android.net.Uri; import android.preference.PreferenceManager; -import android.provider.Browser; import android.support.annotation.Nullable; import android.support.customtabs.CustomTabsIntent; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; @@ -109,7 +107,7 @@ public class LinkHelper { customSpan = new CustomURLSpan(span.getURL()) { @Override public void onClick(View widget) { - listener.onViewURL(getURL()); + listener.onViewUrl(getURL()); } }; } diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 448f5ea1a..5c2efaf3d 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -232,4 +232,6 @@ android:contentDescription="@string/action_mention" app:srcCompat="@drawable/ic_create_24dp" /> + + diff --git a/app/src/main/res/layout/activity_favourites.xml b/app/src/main/res/layout/activity_favourites.xml index bb028eca5..6048aaecc 100644 --- a/app/src/main/res/layout/activity_favourites.xml +++ b/app/src/main/res/layout/activity_favourites.xml @@ -17,4 +17,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index e3cabb067..997db0723 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -90,4 +90,6 @@ app:layout_anchorGravity="bottom|end" app:srcCompat="@drawable/ic_create_24dp" /> + + diff --git a/app/src/main/res/layout/activity_modal_timeline.xml b/app/src/main/res/layout/activity_modal_timeline.xml index 997a39a13..cd565ac1c 100644 --- a/app/src/main/res/layout/activity_modal_timeline.xml +++ b/app/src/main/res/layout/activity_modal_timeline.xml @@ -1,27 +1,22 @@ - - - - + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + + + + + diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index 9c6671a02..c9f1a1ee5 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -31,4 +31,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_tag.xml b/app/src/main/res/layout/activity_view_tag.xml index c9812048f..474264fa5 100644 --- a/app/src/main/res/layout/activity_view_tag.xml +++ b/app/src/main/res/layout/activity_view_tag.xml @@ -17,4 +17,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_thread.xml b/app/src/main/res/layout/activity_view_thread.xml index 464906edb..65d3838b0 100644 --- a/app/src/main/res/layout/activity_view_thread.xml +++ b/app/src/main/res/layout/activity_view_thread.xml @@ -17,4 +17,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml index 724a69cd1..f2ed42959 100644 --- a/app/src/main/res/layout/fragment_search.xml +++ b/app/src/main/res/layout/fragment_search.xml @@ -26,6 +26,4 @@ android:text="@string/search_no_results" android:visibility="gone" /> - - diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml index b743e8f5a..1f3fa1ed4 100644 --- a/app/src/main/res/layout/fragment_timeline.xml +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -1,19 +1,12 @@ - - + android:layout_gravity="top"> - - - - \ No newline at end of file + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml index df0dd9758..910c8238d 100644 --- a/app/src/main/res/layout/fragment_view_thread.xml +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -1,19 +1,13 @@ - - + + - - - - - + android:scrollbars="vertical" /> + diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/SFragmentTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt similarity index 75% rename from app/src/test/java/com/keylesspalace/tusky/fragment/SFragmentTest.kt rename to app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 9d26cbcdb..ca6930979 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/SFragmentTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -13,14 +13,15 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.fragment +package com.keylesspalace.tusky +import android.support.design.widget.BottomSheetBehavior import android.text.SpannedString +import android.widget.LinearLayout import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.SearchResults import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases import okhttp3.Request import org.junit.Assert import org.junit.Before @@ -35,8 +36,8 @@ import retrofit2.Callback import retrofit2.Response import java.util.* -class SFragmentTest { - private lateinit var fragment : FakeSFragment +class BottomSheetActivityTest { + private lateinit var activity : FakeBottomSheetActivity private lateinit var apiMock: MastodonApi private val accountQuery = "http://mastodon.foo.bar/@User" private val statusQuery = "http://mastodon.foo.bar/@User/345678" @@ -85,13 +86,12 @@ class SFragmentTest { @Before fun setup() { - fragment = FakeSFragment() - 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) - fragment.mastodonApi = apiMock + + activity = FakeBottomSheetActivity(apiMock) } @RunWith(Parameterized::class) @@ -131,22 +131,22 @@ class SFragmentTest { @Test fun test() { - Assert.assertEquals(expectedResult, SFragment.looksLikeMastodonUrl(url)) + Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url)) } } @Test fun beginEndSearch_setIsSearching_isSearchingAfterBegin() { - fragment.onBeginSearch("https://mastodon.foo.bar/@User") - Assert.assertTrue(fragment.isSearching) + activity.onBeginSearch("https://mastodon.foo.bar/@User") + Assert.assertTrue(activity.isSearching()) } @Test fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() { val validUrl = "https://mastodon.foo.bar/@User" - fragment.onBeginSearch(validUrl) - fragment.onEndSearch(validUrl) - Assert.assertFalse(fragment.isSearching) + activity.onBeginSearch(validUrl) + activity.onEndSearch(validUrl) + Assert.assertFalse(activity.isSearching()) } @Test @@ -154,18 +154,18 @@ class SFragmentTest { val validUrl = "https://mastodon.foo.bar/@User" val invalidUrl = "" - fragment.onBeginSearch(validUrl) - fragment.onEndSearch(invalidUrl) - Assert.assertTrue(fragment.isSearching) + activity.onBeginSearch(validUrl) + activity.onEndSearch(invalidUrl) + Assert.assertTrue(activity.isSearching()) } @Test fun cancelActiveSearch() { val url = "https://mastodon.foo.bar/@User" - fragment.onBeginSearch(url) - fragment.cancelActiveSearch() - Assert.assertFalse(fragment.isSearching) + activity.onBeginSearch(url) + activity.cancelActiveSearch() + Assert.assertFalse(activity.isSearching()) } @Test @@ -173,85 +173,84 @@ class SFragmentTest { val firstUrl = "https://mastodon.foo.bar/@User" val secondUrl = "https://mastodon.foo.bar/@meh" - fragment.onBeginSearch(firstUrl) - fragment.cancelActiveSearch() + activity.onBeginSearch(firstUrl) + activity.cancelActiveSearch() - fragment.onBeginSearch(secondUrl) - Assert.assertTrue(fragment.getCancelSearchRequested(firstUrl)) - Assert.assertFalse(fragment.getCancelSearchRequested(secondUrl)) + activity.onBeginSearch(secondUrl) + Assert.assertTrue(activity.getCancelSearchRequested(firstUrl)) + Assert.assertFalse(activity.getCancelSearchRequested(secondUrl)) } @Test fun search_inIdealConditions_returnsRequestedResults_forAccount() { - fragment.onViewURL(accountQuery) + activity.viewUrl(accountQuery) accountCallback.invokeCallback() - Assert.assertEquals(account.id, fragment.accountId) + Assert.assertEquals(account.id, activity.accountId) } @Test fun search_inIdealConditions_returnsRequestedResults_forStatus() { - fragment.onViewURL(statusQuery) + activity.viewUrl(statusQuery) statusCallback.invokeCallback() - Assert.assertEquals(status, fragment.status) + Assert.assertEquals(status, activity.status) } @Test fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() { - fragment.onViewURL(nonMastodonQuery) + activity.viewUrl(nonMastodonQuery) emptyCallback.invokeCallback() - Assert.assertEquals(nonMastodonQuery, fragment.url) + Assert.assertEquals(nonMastodonQuery, activity.link) } @Test fun search_withCancellation_doesNotLoadUrl_forAccount() { - fragment.onViewURL(accountQuery) - Assert.assertTrue(fragment.isSearching) - fragment.cancelActiveSearch() - Assert.assertFalse(fragment.isSearching) + activity.viewUrl(accountQuery) + Assert.assertTrue(activity.isSearching()) + activity.cancelActiveSearch() + Assert.assertFalse(activity.isSearching()) accountCallback.invokeCallback() - Assert.assertEquals(null, fragment.accountId) + Assert.assertEquals(null, activity.accountId) } @Test fun search_withCancellation_doesNotLoadUrl_forStatus() { - fragment.onViewURL(accountQuery) - fragment.cancelActiveSearch() + activity.viewUrl(accountQuery) + activity.cancelActiveSearch() accountCallback.invokeCallback() - Assert.assertEquals(null, fragment.accountId) + Assert.assertEquals(null, activity.accountId) } @Test fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() { - fragment.onViewURL(nonMastodonQuery) - fragment.cancelActiveSearch() + activity.viewUrl(nonMastodonQuery) + activity.cancelActiveSearch() emptyCallback.invokeCallback() - Assert.assertEquals(null, fragment.url) + Assert.assertEquals(null, activity.searchUrl) } @Test fun search_withPreviousCancellation_completes() { // begin/cancel account search - fragment.onViewURL(accountQuery) - fragment.cancelActiveSearch() + activity.viewUrl(accountQuery) + activity.cancelActiveSearch() // begin status search - fragment.onViewURL(statusQuery) + activity.viewUrl(statusQuery) // return response from account search accountCallback.invokeCallback() // ensure that status search is still ongoing - Assert.assertTrue(fragment.isSearching) + Assert.assertTrue(activity.isSearching()) statusCallback.invokeCallback() // ensure that the result of the status search was recorded // and the account search wasn't - Assert.assertEquals(status, fragment.status) - Assert.assertEquals(null, fragment.accountId) + Assert.assertEquals(status, activity.status) + Assert.assertEquals(null, activity.accountId) } - class FakeSearchResults : Call - { + class FakeSearchResults : Call { private var searchResults: SearchResults private var callback: Callback? = null @@ -283,29 +282,33 @@ class SFragmentTest { override fun request(): Request { throw NotImplementedError() } } - class FakeSFragment : SFragment() { + class FakeBottomSheetActivity(val api: MastodonApi) : BottomSheetActivity() { + var status: Status? = null var accountId: String? = null - var url: String? = null + var link: String? = null init { - callList = mutableListOf() + @Suppress("UNCHECKED_CAST") + bottomSheet = Mockito.mock(BottomSheetBehavior::class.java) as BottomSheetBehavior + callList = arrayListOf() + } + + override fun getMastodonApi(): MastodonApi { + return api } override fun openLink(url: String) { - this.url = url + this.link = url } - override fun viewAccount(id: String?) { - accountId = id + override fun viewAccount(id: String) { + this.accountId = id } - override fun viewThread(status: Status?) { + override fun viewThread(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() } } } \ No newline at end of file