View links to statuses inside Tusky (#568)
* View links to statuses inside Tusky * Only attempt to open links that look like mastodon statuses * Add support for pleroma statuses * Move "smells like mastodon" url check to click handler * Add bottom sheet to notify users of post query status * Improve architecture for managing search status * Push everything into SFragment * Add external lookup for non-locally-resolved account links * Clean up copypasta from LinkHelper.setClickableText * Apply PR feedback * Migrate bottom sheet wrappers to CoordinatorLayout
This commit is contained in:
parent
3f71c5495f
commit
76eae44324
13 changed files with 257 additions and 58 deletions
|
@ -329,6 +329,11 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA
|
||||||
intent.putExtra("id", id);
|
intent.putExtra("id", id);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewURL(String url) {
|
||||||
|
LinkHelper.openLink(url, note.getContext());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (account.getLocked()) {
|
if (account.getLocked()) {
|
||||||
|
|
|
@ -39,6 +39,7 @@ import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.SearchResults;
|
import com.keylesspalace.tusky.entity.SearchResults;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -141,6 +142,11 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewURL(String url) {
|
||||||
|
LinkHelper.openLink(url, getApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
private void handleIntent(Intent intent) {
|
private void handleIntent(Intent intent) {
|
||||||
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
|
||||||
currentQuery = intent.getStringExtra(SearchManager.QUERY);
|
currentQuery = intent.getStringExtra(SearchManager.QUERY);
|
||||||
|
|
|
@ -48,7 +48,6 @@ import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
import com.keylesspalace.tusky.util.CollectionUtil;
|
import com.keylesspalace.tusky.util.CollectionUtil;
|
||||||
|
@ -106,8 +105,6 @@ public class NotificationsFragment extends SFragment implements
|
||||||
@Inject
|
@Inject
|
||||||
public TimelineCases timelineCases;
|
public TimelineCases timelineCases;
|
||||||
@Inject
|
@Inject
|
||||||
public MastodonApi mastodonApi;
|
|
||||||
@Inject
|
|
||||||
AccountManager accountManager;
|
AccountManager accountManager;
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
|
|
|
@ -20,12 +20,15 @@ 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.AccountActivity;
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
import com.keylesspalace.tusky.ComposeActivity;
|
||||||
|
@ -38,15 +41,28 @@ 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.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 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
|
||||||
|
@ -58,8 +74,13 @@ 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;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
protected MastodonApi mastodonApi;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
@ -70,6 +91,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
loggedInAccountId = activeAccount.getAccountId();
|
loggedInAccountId = activeAccount.getAccountId();
|
||||||
loggedInUsername = activeAccount.getUsername();
|
loggedInUsername = activeAccount.getUsername();
|
||||||
}
|
}
|
||||||
|
setupBottomSheet(getView());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -208,11 +230,13 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void viewThread(Status status) {
|
protected void viewThread(Status status) {
|
||||||
|
if (!isSearching()) {
|
||||||
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
|
Intent intent = new Intent(getContext(), ViewThreadActivity.class);
|
||||||
intent.putExtra("id", status.getActionableId());
|
intent.putExtra("id", status.getActionableId());
|
||||||
intent.putExtra("url", status.getActionableStatus().getUrl());
|
intent.putExtra("url", status.getActionableStatus().getUrl());
|
||||||
startActivity(intent);
|
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);
|
||||||
|
@ -235,4 +259,140 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov
|
||||||
intent.putExtra("status_content", HtmlUtils.toHtml(statusContent));
|
intent.putExtra("status_content", HtmlUtils.toHtml(statusContent));
|
||||||
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
|
||||||
|
private 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("^/(@|notice)[^/]*/\\d+$") ||
|
||||||
|
path.matches("^/objects/[-a-f0-9]+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBeginSearch(@NonNull String url) {
|
||||||
|
searchUrl = url;
|
||||||
|
showQuerySheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean getCancelSearchRequested(@NonNull String url) {
|
||||||
|
return !url.equals(searchUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isSearching() {
|
||||||
|
return searchUrl != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cancelActiveSearch()
|
||||||
|
{
|
||||||
|
if (isSearching()) {
|
||||||
|
onEndSearch(searchUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onViewURL(String url) {
|
||||||
|
if (!looksLikeMastodonUrl(url)) {
|
||||||
|
LinkHelper.openLink(url, getContext());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinkHelper.openLink(url, getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<SearchResults> call, @NonNull Throwable t) {
|
||||||
|
if (!getCancelSearchRequested(url)) {
|
||||||
|
onEndSearch(url);
|
||||||
|
LinkHelper.openLink(url, getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,8 +99,6 @@ public class TimelineFragment extends SFragment implements
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
TimelineCases timelineCases;
|
TimelineCases timelineCases;
|
||||||
@Inject
|
|
||||||
MastodonApi mastodonApi;
|
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private TimelineAdapter adapter;
|
private TimelineAdapter adapter;
|
||||||
|
|
|
@ -43,7 +43,6 @@ import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.entity.StatusContext;
|
import com.keylesspalace.tusky.entity.StatusContext;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||||
import com.keylesspalace.tusky.util.PairedList;
|
import com.keylesspalace.tusky.util.PairedList;
|
||||||
|
@ -68,8 +67,6 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public TimelineCases timelineCases;
|
public TimelineCases timelineCases;
|
||||||
@Inject
|
|
||||||
public MastodonApi mastodonApi;
|
|
||||||
|
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
|
|
|
@ -18,4 +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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.ClickableSpan
|
||||||
|
|
||||||
|
abstract class ClickableSpanNoUnderline : ClickableSpan() {
|
||||||
|
override fun updateDrawState(ds: TextPaint?) {
|
||||||
|
super.updateDrawState(ds)
|
||||||
|
ds?.isUnderlineText = false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -74,20 +74,14 @@ public class LinkHelper {
|
||||||
int end = builder.getSpanEnd(span);
|
int end = builder.getSpanEnd(span);
|
||||||
int flags = builder.getSpanFlags(span);
|
int flags = builder.getSpanFlags(span);
|
||||||
CharSequence text = builder.subSequence(start, end);
|
CharSequence text = builder.subSequence(start, end);
|
||||||
|
ClickableSpan customSpan = null;
|
||||||
|
|
||||||
if (text.charAt(0) == '#') {
|
if (text.charAt(0) == '#') {
|
||||||
final String tag = text.subSequence(1, text.length()).toString();
|
final String tag = text.subSequence(1, text.length()).toString();
|
||||||
ClickableSpan newSpan = new ClickableSpan() {
|
customSpan = new ClickableSpanNoUnderline() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View widget) {
|
public void onClick(View widget) { listener.onViewTag(tag); }
|
||||||
listener.onViewTag(tag);
|
|
||||||
}
|
|
||||||
@Override public void updateDrawState(TextPaint ds) {
|
|
||||||
super.updateDrawState(ds);
|
|
||||||
ds.setUnderlineText(false);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
builder.removeSpan(span);
|
|
||||||
builder.setSpan(newSpan, start, end, flags);
|
|
||||||
} else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) {
|
} else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) {
|
||||||
String accountUsername = text.subSequence(1, text.length()).toString();
|
String accountUsername = text.subSequence(1, text.length()).toString();
|
||||||
/* There may be multiple matches for users on different instances with the same
|
/* There may be multiple matches for users on different instances with the same
|
||||||
|
@ -104,28 +98,23 @@ public class LinkHelper {
|
||||||
}
|
}
|
||||||
if (id != null) {
|
if (id != null) {
|
||||||
final String accountId = id;
|
final String accountId = id;
|
||||||
ClickableSpan newSpan = new ClickableSpan() {
|
customSpan = new ClickableSpanNoUnderline() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) { listener.onViewAccount(accountId); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (customSpan == null) {
|
||||||
|
customSpan = new CustomURLSpan(span.getURL()) {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View widget) {
|
public void onClick(View widget) {
|
||||||
listener.onViewAccount(accountId);
|
listener.onViewURL(getURL());
|
||||||
}
|
|
||||||
@Override public void updateDrawState(TextPaint ds) {
|
|
||||||
super.updateDrawState(ds);
|
|
||||||
ds.setUnderlineText(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
builder.removeSpan(span);
|
|
||||||
builder.setSpan(newSpan, start, end, flags);
|
|
||||||
} else {
|
|
||||||
ClickableSpan newSpan = new CustomURLSpan(span.getURL());
|
|
||||||
builder.removeSpan(span);
|
|
||||||
builder.setSpan(newSpan, start, end, flags);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ClickableSpan newSpan = new CustomURLSpan(span.getURL());
|
|
||||||
builder.removeSpan(span);
|
builder.removeSpan(span);
|
||||||
builder.setSpan(newSpan, start, end, flags);
|
builder.setSpan(customSpan, start, end, flags);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
view.setText(builder);
|
view.setText(builder);
|
||||||
view.setLinksClickable(true);
|
view.setLinksClickable(true);
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.design.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_width="match_parent">
|
||||||
<android.support.v4.widget.SwipeRefreshLayout
|
<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:id="@+id/swipe_refresh_layout"
|
||||||
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.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,8 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.design.widget.CoordinatorLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_width="match_parent">
|
||||||
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<android.support.v4.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/swipe_refresh_layout"
|
android:id="@+id/swipe_refresh_layout"
|
||||||
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.support.v7.widget.RecyclerView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@+id/recycler_view"
|
android:id="@+id/recycler_view"
|
||||||
|
@ -10,3 +15,5 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:scrollbars="vertical" />
|
android:scrollbars="vertical" />
|
||||||
</android.support.v4.widget.SwipeRefreshLayout>
|
</android.support.v4.widget.SwipeRefreshLayout>
|
||||||
|
<include layout="@layout/item_status_bottom_sheet" />
|
||||||
|
</android.support.design.widget.CoordinatorLayout>
|
||||||
|
|
21
app/src/main/res/layout/item_status_bottom_sheet.xml
Normal file
21
app/src/main/res/layout/item_status_bottom_sheet.xml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/item_status_bottom_sheet"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="?android:colorBackground"
|
||||||
|
app:behavior_hideable="true"
|
||||||
|
app:layout_behavior="android.support.design.widget.BottomSheetBehavior"
|
||||||
|
>
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="@string/performing_lookup_title"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
android:textColor="?android:textColorPrimary"
|
||||||
|
/>
|
||||||
|
</LinearLayout>
|
|
@ -303,5 +303,6 @@
|
||||||
|
|
||||||
<string name="error_no_custom_emojis">Your instance %s does not have any custom emojis</string>
|
<string name="error_no_custom_emojis">Your instance %s does not have any custom emojis</string>
|
||||||
<string name="copy_to_clipboard_success">Copied to clipboard</string>
|
<string name="copy_to_clipboard_success">Copied to clipboard</string>
|
||||||
|
<string name="performing_lookup_title">Performing lookup...</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Reference in a new issue