Link previews for detail statuses (#424)

* implement link preview cards on detail statuses

* cleanup code
This commit is contained in:
Konrad Pozniak 2017-10-27 13:20:17 +02:00 committed by GitHub
parent df4dfa7766
commit 5cbc7217ff
14 changed files with 420 additions and 35 deletions

View file

@ -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);
}
} }
} }

View file

@ -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<>();

View 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];
}
};
}

View file

@ -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();

View file

@ -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
);
} }

View file

@ -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());
}
} }
} }

View file

@ -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());
}
}
} }

View file

@ -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);
} }
} }
} }

View 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>

View 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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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">