Link previews for detail statuses (#424)
* implement link preview cards on detail statuses * cleanup code
This commit is contained in:
parent
df4dfa7766
commit
5cbc7217ff
14 changed files with 420 additions and 35 deletions
|
@ -1,20 +1,28 @@
|
||||||
package com.keylesspalace.tusky.adapter;
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.CustomTabURLSpan;
|
import com.keylesspalace.tusky.util.CustomTabURLSpan;
|
||||||
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
@ -23,12 +31,24 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
private TextView reblogs;
|
private TextView reblogs;
|
||||||
private TextView favourites;
|
private TextView favourites;
|
||||||
private TextView application;
|
private TextView application;
|
||||||
|
private LinearLayout cardView;
|
||||||
|
private LinearLayout cardInfo;
|
||||||
|
private ImageView cardImage;
|
||||||
|
private TextView cardTitle;
|
||||||
|
private TextView cardDescription;
|
||||||
|
private TextView cardUrl;
|
||||||
|
|
||||||
StatusDetailedViewHolder(View view) {
|
StatusDetailedViewHolder(View view) {
|
||||||
super(view);
|
super(view);
|
||||||
reblogs = view.findViewById(R.id.status_reblogs);
|
reblogs = view.findViewById(R.id.status_reblogs);
|
||||||
favourites = view.findViewById(R.id.status_favourites);
|
favourites = view.findViewById(R.id.status_favourites);
|
||||||
application = view.findViewById(R.id.status_application);
|
application = view.findViewById(R.id.status_application);
|
||||||
|
cardView = view.findViewById(R.id.card_view);
|
||||||
|
cardInfo = view.findViewById(R.id.card_info);
|
||||||
|
cardImage = view.findViewById(R.id.card_image);
|
||||||
|
cardTitle = view.findViewById(R.id.card_title);
|
||||||
|
cardDescription = view.findViewById(R.id.card_description);
|
||||||
|
cardUrl = view.findViewById(R.id.card_link);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -65,11 +85,68 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
void setupWithStatus(StatusViewData status, final StatusActionListener listener,
|
void setupWithStatus(final StatusViewData status, final StatusActionListener listener,
|
||||||
boolean mediaPreviewEnabled) {
|
boolean mediaPreviewEnabled) {
|
||||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||||
reblogs.setText(status.getReblogsCount());
|
reblogs.setText(status.getReblogsCount());
|
||||||
favourites.setText(status.getFavouritesCount());
|
favourites.setText(status.getFavouritesCount());
|
||||||
setApplication(status.getApplication());
|
setApplication(status.getApplication());
|
||||||
|
|
||||||
|
if(status.getAttachments().length == 0 && status.getCard() != null && !TextUtils.isEmpty(status.getCard().url)) {
|
||||||
|
final Card card = status.getCard();
|
||||||
|
cardView.setVisibility(View.VISIBLE);
|
||||||
|
cardTitle.setText(card.title);
|
||||||
|
cardDescription.setText(card.description);
|
||||||
|
|
||||||
|
cardUrl.setText(card.url);
|
||||||
|
|
||||||
|
if(card.width > 0 && card.height > 0 && !TextUtils.isEmpty(card.image)) {
|
||||||
|
cardImage.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
if(card.width > card.height) {
|
||||||
|
cardView.setOrientation(LinearLayout.VERTICAL);
|
||||||
|
cardImage.getLayoutParams().height = cardImage.getContext().getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.card_image_vertical_height);
|
||||||
|
cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
} else {
|
||||||
|
cardView.setOrientation(LinearLayout.HORIZONTAL);
|
||||||
|
cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
cardImage.getLayoutParams().width = cardImage.getContext().getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.card_image_horizontal_width);
|
||||||
|
cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
cardView.setClipToOutline(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Picasso.with(cardImage.getContext())
|
||||||
|
.load(card.image)
|
||||||
|
.fit()
|
||||||
|
.centerCrop()
|
||||||
|
.into(cardImage);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cardImage.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardView.setOnClickListener(new View.OnClickListener() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
|
||||||
|
LinkHelper.openLink(card.url, v.getContext());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cardView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
@ -37,6 +38,8 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
private int detailedStatusPosition;
|
private int detailedStatusPosition;
|
||||||
|
|
||||||
|
private Card detailedStatusCard;
|
||||||
|
|
||||||
public ThreadAdapter(StatusActionListener listener) {
|
public ThreadAdapter(StatusActionListener listener) {
|
||||||
this.statusActionListener = listener;
|
this.statusActionListener = listener;
|
||||||
this.statuses = new ArrayList<>();
|
this.statuses = new ArrayList<>();
|
||||||
|
|
96
app/src/main/java/com/keylesspalace/tusky/entity/Card.java
Normal file
96
app/src/main/java/com/keylesspalace/tusky/entity/Card.java
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.entity;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName;
|
||||||
|
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||||
|
|
||||||
|
public class Card implements Parcelable {
|
||||||
|
|
||||||
|
public String url;
|
||||||
|
|
||||||
|
public String title;
|
||||||
|
|
||||||
|
public String description;
|
||||||
|
|
||||||
|
public String image;
|
||||||
|
|
||||||
|
public String type;
|
||||||
|
|
||||||
|
public int width;
|
||||||
|
|
||||||
|
public int height;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return url.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object other) {
|
||||||
|
if (this.url == null) {
|
||||||
|
return this == other;
|
||||||
|
} else if (!(other instanceof Card)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Card account = (Card) other;
|
||||||
|
return account.url.equals(this.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int describeContents() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void writeToParcel(Parcel dest, int flags) {
|
||||||
|
dest.writeString(url);
|
||||||
|
dest.writeString(title);
|
||||||
|
dest.writeString(description);
|
||||||
|
dest.writeString(image);
|
||||||
|
dest.writeString(type);
|
||||||
|
dest.writeInt(width);
|
||||||
|
dest.writeInt(height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Card() {}
|
||||||
|
|
||||||
|
private Card(Parcel in) {
|
||||||
|
url = in.readString();
|
||||||
|
title = in.readString();
|
||||||
|
description = in.readString();
|
||||||
|
image = in.readString();
|
||||||
|
type = in.readString();
|
||||||
|
width = in.readInt();
|
||||||
|
height = in.readInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final Creator<Card> CREATOR = new Creator<Card>() {
|
||||||
|
@Override
|
||||||
|
public Card createFromParcel(Parcel source) {
|
||||||
|
return new Card(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Card[] newArray(int size) {
|
||||||
|
return new Card[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ import android.view.ViewGroup;
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
import com.keylesspalace.tusky.adapter.ThreadAdapter;
|
||||||
|
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;
|
||||||
|
@ -64,6 +65,7 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
private ThreadAdapter adapter;
|
private ThreadAdapter adapter;
|
||||||
private String thisThreadsStatusId;
|
private String thisThreadsStatusId;
|
||||||
private TimelineReceiver timelineReceiver;
|
private TimelineReceiver timelineReceiver;
|
||||||
|
private Card card;
|
||||||
|
|
||||||
private int statusIndex = 0;
|
private int statusIndex = 0;
|
||||||
|
|
||||||
|
@ -135,6 +137,7 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
public void onRefresh() {
|
public void onRefresh() {
|
||||||
sendStatusRequest(thisThreadsStatusId);
|
sendStatusRequest(thisThreadsStatusId);
|
||||||
sendThreadRequest(thisThreadsStatusId);
|
sendThreadRequest(thisThreadsStatusId);
|
||||||
|
sendCardRequest(thisThreadsStatusId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -327,6 +330,26 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
callList.add(call);
|
callList.add(call);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendCardRequest(final String id) {
|
||||||
|
Call<Card> call = mastodonApi.statusCard(id);
|
||||||
|
call.enqueue(new Callback<Card>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<Card> call, @NonNull Response<Card> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
showCard(response.body());
|
||||||
|
} else {
|
||||||
|
onThreadRequestFailure(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<Card> call, @NonNull Throwable t) {
|
||||||
|
onThreadRequestFailure(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
callList.add(call);
|
||||||
|
}
|
||||||
|
|
||||||
private void onThreadRequestFailure(final String id) {
|
private void onThreadRequestFailure(final String id) {
|
||||||
View view = getView();
|
View view = getView();
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
@ -356,7 +379,13 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
int i = statusIndex;
|
int i = statusIndex;
|
||||||
statuses.add(i, status);
|
statuses.add(i, status);
|
||||||
adapter.setDetailedStatusPosition(i);
|
adapter.setDetailedStatusPosition(i);
|
||||||
adapter.addItem(i, statuses.getPairedItem(i));
|
StatusViewData viewData = statuses.getPairedItem(i);
|
||||||
|
if(viewData.getCard() == null && card != null) {
|
||||||
|
viewData = new StatusViewData.Builder(viewData)
|
||||||
|
.setCard(card)
|
||||||
|
.createStatusViewData();
|
||||||
|
}
|
||||||
|
adapter.addItem(i, viewData);
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,7 +420,13 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
// In case we needed to delete everything (which is way easier than deleting
|
// In case we needed to delete everything (which is way easier than deleting
|
||||||
// everything except one), re-insert the remaining status here.
|
// everything except one), re-insert the remaining status here.
|
||||||
statuses.add(statusIndex, mainStatus);
|
statuses.add(statusIndex, mainStatus);
|
||||||
adapter.addItem(statusIndex, statuses.getPairedItem(statusIndex));
|
StatusViewData viewData = statuses.getPairedItem(statusIndex);
|
||||||
|
if(viewData.getCard() == null && card != null) {
|
||||||
|
viewData = new StatusViewData.Builder(viewData)
|
||||||
|
.setCard(card)
|
||||||
|
.createStatusViewData();
|
||||||
|
}
|
||||||
|
adapter.addItem(statusIndex, viewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert newly fetched descendants
|
// Insert newly fetched descendants
|
||||||
|
@ -410,6 +445,21 @@ public class ViewThreadFragment extends SFragment implements
|
||||||
adapter.addAll(descendantsViewData);
|
adapter.addAll(descendantsViewData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showCard(Card card) {
|
||||||
|
this.card = card;
|
||||||
|
if(statuses.size() != 0) {
|
||||||
|
StatusViewData oldViewData = statuses.getPairedItem(statusIndex);
|
||||||
|
if(oldViewData != null) {
|
||||||
|
StatusViewData newViewData = new StatusViewData.Builder(statuses.getPairedItem(statusIndex))
|
||||||
|
.setCard(card)
|
||||||
|
.createStatusViewData();
|
||||||
|
|
||||||
|
statuses.setPairedItem(statusIndex, newViewData);
|
||||||
|
adapter.setItem(statusIndex, newViewData, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
adapter.clear();
|
adapter.clear();
|
||||||
|
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.network;
|
||||||
import com.keylesspalace.tusky.entity.AccessToken;
|
import com.keylesspalace.tusky.entity.AccessToken;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials;
|
import com.keylesspalace.tusky.entity.AppCredentials;
|
||||||
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Media;
|
import com.keylesspalace.tusky.entity.Media;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Profile;
|
import com.keylesspalace.tusky.entity.Profile;
|
||||||
|
@ -215,4 +216,9 @@ public interface MastodonApi {
|
||||||
@Field("code") String code,
|
@Field("code") String code,
|
||||||
@Field("grant_type") String grantType
|
@Field("grant_type") String grantType
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@GET("/api/v1/statuses/{id}/card")
|
||||||
|
Call<Card> statusCard(
|
||||||
|
@Path("id") String statusId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,11 @@
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
import android.content.ActivityNotFoundException;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.customtabs.CustomTabsIntent;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.text.style.URLSpan;
|
import android.text.style.URLSpan;
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
|
|
||||||
public class CustomTabURLSpan extends URLSpan {
|
public class CustomTabURLSpan extends URLSpan {
|
||||||
public CustomTabURLSpan(String url) {
|
public CustomTabURLSpan(String url) {
|
||||||
super(url);
|
super(url);
|
||||||
|
@ -37,27 +29,8 @@ public class CustomTabURLSpan extends URLSpan {
|
||||||
};
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View widget) {
|
public void onClick(View view) {
|
||||||
Uri uri = Uri.parse(getURL());
|
Uri uri = Uri.parse(getURL());
|
||||||
Context context = widget.getContext();
|
LinkHelper.openLinkInCustomTab(uri, view.getContext());
|
||||||
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("lightTheme", false);
|
|
||||||
int toolbarColor = ContextCompat.getColor(context, lightTheme ? R.color.custom_tab_toolbar_light : R.color.custom_tab_toolbar_dark);
|
|
||||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
|
||||||
builder.setToolbarColor(toolbarColor);
|
|
||||||
CustomTabsIntent customTabsIntent = builder.build();
|
|
||||||
try {
|
|
||||||
String packageName = CustomTabsHelper.getPackageNameToUse(context);
|
|
||||||
|
|
||||||
//If we cant find a package name, it means theres no browser that supports
|
|
||||||
//Chrome Custom Tabs installed. So, we fallback to the webview
|
|
||||||
if (packageName == null) {
|
|
||||||
super.onClick(widget);
|
|
||||||
} else {
|
|
||||||
customTabsIntent.intent.setPackage(packageName);
|
|
||||||
customTabsIntent.launchUrl(context, uri);
|
|
||||||
}
|
|
||||||
} catch (ActivityNotFoundException e) {
|
|
||||||
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,25 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
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.annotation.Nullable;
|
||||||
|
import android.support.customtabs.CustomTabsIntent;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
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;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
|
|
||||||
|
@ -111,4 +121,71 @@ public class LinkHelper {
|
||||||
view.setLinksClickable(true);
|
view.setLinksClickable(true);
|
||||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
||||||
|
*
|
||||||
|
* @param url a string containing the url to open
|
||||||
|
* @param context context
|
||||||
|
*/
|
||||||
|
public static void openLink(String url, Context context) {
|
||||||
|
Uri uri = Uri.parse(url);
|
||||||
|
|
||||||
|
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean("customTabs", true);
|
||||||
|
if (useCustomTabs) {
|
||||||
|
openLinkInCustomTab(uri, context);
|
||||||
|
} else {
|
||||||
|
openLinkInBrowser(uri, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* opens a link in the browser via Intent.ACTION_VIEW
|
||||||
|
*
|
||||||
|
* @param uri the uri to open
|
||||||
|
* @param context context
|
||||||
|
*/
|
||||||
|
public static void openLinkInBrowser(Uri uri, Context context) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
|
||||||
|
intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
|
||||||
|
try {
|
||||||
|
context.startActivity(intent);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.w("URLSpan", "Actvity was not found for intent, " + intent.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* tries to open a link in a custom tab
|
||||||
|
* falls back to browser if not possible
|
||||||
|
*
|
||||||
|
* @param uri the uri to open
|
||||||
|
* @param context context
|
||||||
|
*/
|
||||||
|
public static void openLinkInCustomTab(Uri uri, Context context) {
|
||||||
|
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("lightTheme", false);
|
||||||
|
int toolbarColor = ContextCompat.getColor(context, lightTheme ? R.color.custom_tab_toolbar_light : R.color.custom_tab_toolbar_dark);
|
||||||
|
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||||
|
builder.setToolbarColor(toolbarColor);
|
||||||
|
CustomTabsIntent customTabsIntent = builder.build();
|
||||||
|
try {
|
||||||
|
String packageName = CustomTabsHelper.getPackageNameToUse(context);
|
||||||
|
|
||||||
|
//If we cant find a package name, it means theres no browser that supports
|
||||||
|
//Chrome Custom Tabs installed. So, we fallback to the webview
|
||||||
|
if (packageName == null) {
|
||||||
|
openLinkInBrowser(uri, context);
|
||||||
|
} else {
|
||||||
|
customTabsIntent.intent.setPackage(packageName);
|
||||||
|
customTabsIntent.launchUrl(context, uri);
|
||||||
|
}
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package com.keylesspalace.tusky.viewdata;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Card;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -44,6 +45,8 @@ public final class StatusViewData {
|
||||||
private final boolean rebloggingEnabled;
|
private final boolean rebloggingEnabled;
|
||||||
private final Status.Application application;
|
private final Status.Application application;
|
||||||
private final List<Status.Emoji> emojis;
|
private final List<Status.Emoji> emojis;
|
||||||
|
@Nullable
|
||||||
|
private final Card card;
|
||||||
|
|
||||||
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
|
public StatusViewData(String id, Spanned content, boolean reblogged, boolean favourited,
|
||||||
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
|
String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments,
|
||||||
|
@ -51,7 +54,7 @@ public final class StatusViewData {
|
||||||
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
|
boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar,
|
||||||
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId,
|
Date createdAt, String reblogsCount, String favouritesCount, String inReplyToId,
|
||||||
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
|
||||||
Status.Application application, List<Status.Emoji> emojis) {
|
Status.Application application, List<Status.Emoji> emojis, Card card) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
this.reblogged = reblogged;
|
this.reblogged = reblogged;
|
||||||
|
@ -76,6 +79,7 @@ public final class StatusViewData {
|
||||||
this.rebloggingEnabled = rebloggingEnabled;
|
this.rebloggingEnabled = rebloggingEnabled;
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.emojis = emojis;
|
this.emojis = emojis;
|
||||||
|
this.card = card;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
|
@ -179,6 +183,10 @@ public final class StatusViewData {
|
||||||
return emojis;
|
return emojis;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Card getCard() {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
private String id;
|
private String id;
|
||||||
private Spanned content;
|
private Spanned content;
|
||||||
|
@ -204,6 +212,7 @@ public final class StatusViewData {
|
||||||
private boolean rebloggingEnabled;
|
private boolean rebloggingEnabled;
|
||||||
private Status.Application application;
|
private Status.Application application;
|
||||||
private List<Status.Emoji> emojis;
|
private List<Status.Emoji> emojis;
|
||||||
|
private Card card;
|
||||||
|
|
||||||
public Builder() {
|
public Builder() {
|
||||||
}
|
}
|
||||||
|
@ -233,6 +242,8 @@ public final class StatusViewData {
|
||||||
rebloggingEnabled = viewData.rebloggingEnabled;
|
rebloggingEnabled = viewData.rebloggingEnabled;
|
||||||
application = viewData.application;
|
application = viewData.application;
|
||||||
emojis = viewData.getEmojis();
|
emojis = viewData.getEmojis();
|
||||||
|
card = viewData.getCard();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Builder setId(String id) {
|
public Builder setId(String id) {
|
||||||
|
@ -355,12 +366,17 @@ public final class StatusViewData {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Builder setCard(Card card) {
|
||||||
|
this.card = card;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public StatusViewData createStatusViewData() {
|
public StatusViewData createStatusViewData() {
|
||||||
if (this.emojis == null) emojis = Collections.emptyList();
|
if (this.emojis == null) emojis = Collections.emptyList();
|
||||||
return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility,
|
return new StatusViewData(id, content, reblogged, favourited, spoilerText, visibility,
|
||||||
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
|
||||||
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount,
|
||||||
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis);
|
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis, card);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
app/src/main/res/drawable/card_frame_dark.xml
Normal file
6
app/src/main/res/drawable/card_frame_dark.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="5dp" />
|
||||||
|
<stroke android:color="@color/text_color_tertiary_dark" android:width="1px" />
|
||||||
|
<padding android:bottom="1px" android:top="1px" android:left="1px" android:right="1px"/>
|
||||||
|
</shape>
|
6
app/src/main/res/drawable/card_frame_light.xml
Normal file
6
app/src/main/res/drawable/card_frame_light.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="5dp" />
|
||||||
|
<stroke android:color="@color/text_color_tertiary_light" android:width="1px" />
|
||||||
|
<padding android:bottom="1px" android:top="1px" android:left="1px" android:right="1px"/>
|
||||||
|
</shape>
|
|
@ -104,11 +104,72 @@
|
||||||
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
|
android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium"
|
||||||
android:textColor="?android:textColorPrimary" />
|
android:textColor="?android:textColorPrimary" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/card_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_below="@+id/status_content"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_toEndOf="@+id/status_avatar"
|
||||||
|
android:layout_toRightOf="@+id/status_avatar"
|
||||||
|
android:background="?attr/card_background"
|
||||||
|
android:clipChildren="true"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/card_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="300dp"
|
||||||
|
android:background="?attr/card_image_background" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/card_info"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:paddingLeft="6dp"
|
||||||
|
android:paddingRight="6dp"
|
||||||
|
android:paddingTop="6dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/card_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:lines="1"
|
||||||
|
android:textColor="?android:textColorPrimary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/card_description"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="4dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lineSpacingMultiplier="1.1"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:textColor="?android:textColorSecondary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/card_link"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:lines="1"
|
||||||
|
android:textColor="?android:textColorTertiary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/status_media_preview_container"
|
android:id="@+id/status_media_preview_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/status_content"
|
android:layout_below="@+id/card_view"
|
||||||
android:layout_toEndOf="@+id/status_avatar"
|
android:layout_toEndOf="@+id/status_avatar"
|
||||||
android:layout_toRightOf="@+id/status_avatar">
|
android:layout_toRightOf="@+id/status_avatar">
|
||||||
|
|
||||||
|
|
|
@ -39,5 +39,7 @@
|
||||||
<attr name="compose_image_button_tint" format="reference|color" />
|
<attr name="compose_image_button_tint" format="reference|color" />
|
||||||
<attr name="report_status_background_color" format="reference|color" />
|
<attr name="report_status_background_color" format="reference|color" />
|
||||||
<attr name="report_status_divider_drawable" format="reference" />
|
<attr name="report_status_divider_drawable" format="reference" />
|
||||||
|
<attr name="card_background" format="reference|color" />
|
||||||
|
<attr name="card_image_background" format="reference|color" />
|
||||||
|
|
||||||
</resources>
|
</resources>
|
|
@ -21,4 +21,8 @@
|
||||||
<dimen name="status_left_line_margin">38dp</dimen>
|
<dimen name="status_left_line_margin">38dp</dimen>
|
||||||
<dimen name="text_content_margin">16dp</dimen>
|
<dimen name="text_content_margin">16dp</dimen>
|
||||||
<dimen name="status_sensitive_media_button_padding">5dp</dimen>
|
<dimen name="status_sensitive_media_button_padding">5dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="card_image_vertical_height">160dp</dimen>
|
||||||
|
<dimen name="card_image_horizontal_width">100dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -80,6 +80,10 @@
|
||||||
<item name="material_drawer_selected">@color/color_primary_dark</item>
|
<item name="material_drawer_selected">@color/color_primary_dark</item>
|
||||||
<item name="material_drawer_selected_text">@color/text_color_primary_dark</item>
|
<item name="material_drawer_selected_text">@color/text_color_primary_dark</item>
|
||||||
<item name="material_drawer_header_selection_text">@color/text_color_primary_dark</item>
|
<item name="material_drawer_header_selection_text">@color/text_color_primary_dark</item>
|
||||||
|
|
||||||
|
<item name="card_background">@drawable/card_frame_dark</item>
|
||||||
|
<item name="card_image_background">@color/text_color_tertiary_dark</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.ImageButton.Dark" parent="@style/Widget.AppCompat.Button.Borderless.Colored">
|
<style name="AppTheme.ImageButton.Dark" parent="@style/Widget.AppCompat.Button.Borderless.Colored">
|
||||||
|
@ -156,6 +160,10 @@
|
||||||
<item name="material_drawer_selected">@color/color_primary_light</item>
|
<item name="material_drawer_selected">@color/color_primary_light</item>
|
||||||
<item name="material_drawer_selected_text">@color/text_color_primary_light</item>
|
<item name="material_drawer_selected_text">@color/text_color_primary_light</item>
|
||||||
<item name="material_drawer_header_selection_text">@color/text_color_primary_dark</item> <!--Intentionally dark so it can be overlayed over the account's header image.-->
|
<item name="material_drawer_header_selection_text">@color/text_color_primary_dark</item> <!--Intentionally dark so it can be overlayed over the account's header image.-->
|
||||||
|
|
||||||
|
<item name="card_background">@drawable/card_frame_light</item>
|
||||||
|
<item name="card_image_background">@color/text_color_tertiary_light</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.ImageButton.Light" parent="Widget.AppCompat.Button.Borderless.Colored">
|
<style name="AppTheme.ImageButton.Light" parent="Widget.AppCompat.Button.Borderless.Colored">
|
||||||
|
|
Loading…
Reference in a new issue