diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java index a7756baaa..32716179b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.java @@ -312,9 +312,7 @@ public final class AccountActivity extends BaseActivity implements ActionButtonA getSupportActionBar().setSubtitle(subtitle); } - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(this) - .getBoolean("customTabs", false); - LinkHelper.setClickableText(note, account.note, null, useCustomTabs, new LinkListener() { + LinkHelper.setClickableText(note, account.note, null, new LinkListener() { @Override public void onViewTag(String tag) { Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class); diff --git a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java index c27b2f975..1ac3d196a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ComposeActivity.java @@ -78,7 +78,7 @@ import com.keylesspalace.tusky.adapter.MentionAutoCompleteAdapter; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.entity.Account; -import com.keylesspalace.tusky.entity.Media; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.fragment.ComposeOptionsFragment; import com.keylesspalace.tusky.network.ProgressRequestBody; @@ -1274,9 +1274,9 @@ public final class ComposeActivity extends BaseActivity item.uploadRequest = mastodonApi.uploadMedia(body); - item.uploadRequest.enqueue(new Callback() { + item.uploadRequest.enqueue(new Callback() { @Override - public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { if (response.isSuccessful()) { onUploadSuccess(item, response.body()); } else { @@ -1286,14 +1286,14 @@ public final class ComposeActivity extends BaseActivity } @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { + public void onFailure(@NonNull Call call, @NonNull Throwable t) { Log.d(TAG, "Upload request failed. " + t.getMessage()); onUploadFailure(item, call.isCanceled()); } }); } - private void onUploadSuccess(final QueuedMedia item, Media media) { + private void onUploadSuccess(final QueuedMedia item, Attachment media) { item.id = media.id; item.preview.setProgress(-1); item.readyStage = QueuedMedia.ReadyStage.UPLOADED; @@ -1460,7 +1460,7 @@ public final class ComposeActivity extends BaseActivity ProgressImageView preview; Uri uri; String id; - Call uploadRequest; + Call uploadRequest; URLSpan uploadUrl; ReadyStage readyStage; byte[] content; diff --git a/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java index 112189fe6..945aa46f5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SearchActivity.java @@ -20,6 +20,7 @@ import android.app.SearchableInfo; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; import android.support.v7.widget.LinearLayoutManager; @@ -150,8 +151,7 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe } searchView.setOnQueryTextListener(this); - searchView.setFocusable(false); - searchView.setFocusableInTouchMode(false); + searchView.requestFocus(); searchView.setMaxWidth(Integer.MAX_VALUE); } @@ -160,7 +160,7 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe clearResults(); Callback callback = new Callback() { @Override - public void onResponse(Call call, Response response) { + public void onResponse(@NonNull Call call, @NonNull Response response) { if (response.isSuccessful()) { SearchResults results = response.body(); if (results.accounts != null && results.accounts.length > 0 || results.hashtags != null && results.hashtags.length > 0) { @@ -175,7 +175,7 @@ public class SearchActivity extends BaseActivity implements SearchView.OnQueryTe } @Override - public void onFailure(Call call, Throwable t) { + public void onFailure(@NonNull Call call, @NonNull Throwable t) { onSearchFailure(); } }; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 4a7dce80d..f116473fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -19,6 +19,7 @@ import android.content.Context; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.Typeface; +import android.graphics.drawable.Drawable; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; import android.support.v7.widget.RecyclerView; @@ -26,7 +27,6 @@ import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.text.style.StyleSpan; -import android.text.style.URLSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -38,13 +38,18 @@ import android.widget.ToggleButton; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.DateUtils; +import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.view.RoundedTransformation; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; import com.squareup.picasso.Picasso; import java.util.ArrayList; +import java.util.Date; import java.util.List; public class NotificationsAdapter extends RecyclerView.Adapter { @@ -126,13 +131,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case FAVOURITE: case REBLOG: { StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder; - holder.setMessage(type, concreteNotificaton.getAccount().getDisplayName(), - concreteNotificaton.getStatusViewData()); + StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData(); + holder.setDisplayName(statusViewData.getUserFullName()); + holder.setUsername(statusViewData.getNickname()); + holder.setCreatedAt(statusViewData.getCreatedAt()); + + holder.setMessage(concreteNotificaton, statusListener); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().id, concreteNotificaton.getId()); holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(), - concreteNotificaton.getId()); + concreteNotificaton.getAccount().avatar); break; } case FOLLOW: { @@ -220,6 +229,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { void onViewAccount(String id); void onViewStatusForNotificationId(String notificationId); + + void onExpandedChange(boolean expanded, int position); + } private static class FollowViewHolder extends RecyclerView.ViewHolder { @@ -270,30 +282,32 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private static class StatusNotificationViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, ToggleButton.OnCheckedChangeListener { private final TextView message; - private final ImageView icon; + private final TextView displayName; + private final TextView username; + private final TextView timestampInfo; private final TextView statusContent; private final ViewGroup container; private final ImageView statusAvatar; private final ImageView notificationAvatar; - private final ViewGroup topBar; private final View contentWarningBar; private final TextView contentWarningDescriptionTextView; private final ToggleButton contentWarningButton; private String accountId; private String notificationId; - private NotificationActionListener listener; + private NotificationActionListener notificationActionListener; private StatusViewData.Concrete statusViewData; StatusNotificationViewHolder(View itemView) { super(itemView); - message = itemView.findViewById(R.id.notification_text); - icon = itemView.findViewById(R.id.notification_icon); + message = itemView.findViewById(R.id.notification_top_text); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); statusContent = itemView.findViewById(R.id.notification_content); container = itemView.findViewById(R.id.notification_container); statusAvatar = itemView.findViewById(R.id.notification_status_avatar); notificationAvatar = itemView.findViewById(R.id.notification_notification_avatar); - topBar = itemView.findViewById(R.id.notification_top_bar); contentWarningBar = itemView.findViewById(R.id.notification_content_warning_bar); contentWarningDescriptionTextView = itemView.findViewById(R.id.notification_content_warning_description); contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button); @@ -303,33 +317,77 @@ public class NotificationsAdapter extends RecyclerView.Adapter { notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); container.setOnClickListener(this); - topBar.setOnClickListener(this); + message.setOnClickListener(this); + statusContent.setOnClickListener(this); contentWarningButton.setOnCheckedChangeListener(this); } - void setMessage(Notification.Type type, String displayName, - StatusViewData.Concrete status) { - this.statusViewData = status; + private void setDisplayName(String name) { + displayName.setText(name); + } + + private void setUsername(String name) { + Context context = username.getContext(); + String format = context.getString(R.string.status_username_format); + String usernameText = String.format(format, name); + username.setText(usernameText); + } + + private void setCreatedAt(@Nullable Date createdAt) { + // This is the visible timestampInfo. + String readout; + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + CharSequence readoutAloud; + if (createdAt != null) { + long then = createdAt.getTime(); + long now = new Date().getTime(); + readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); + readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, + android.text.format.DateUtils.SECOND_IN_MILLIS, + android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); + } else { + // unknown minutes~ + readout = "?m"; + readoutAloud = "? minutes"; + } + timestampInfo.setText(readout); + timestampInfo.setContentDescription(readoutAloud); + } + + void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { + this.statusViewData = notificationViewData.getStatusViewData(); + + String displayName = notificationViewData.getAccount().getDisplayName(); + Notification.Type type = notificationViewData.getType(); Context context = message.getContext(); String format; + Drawable icon; switch (type) { default: case FAVOURITE: { - icon.setImageResource(R.drawable.ic_star_24dp); - icon.setColorFilter(ContextCompat.getColor(context, - R.color.status_favourite_button_marked_dark)); + icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.status_favourite_button_marked_dark), PorterDuff.Mode.SRC_ATOP); + } + format = context.getString(R.string.notification_favourite_format); break; } case REBLOG: { - icon.setImageResource(R.drawable.ic_repeat_24dp); - icon.setColorFilter(ContextCompat.getColor(context, - R.color.color_accent_dark)); + icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp); + if(icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.color_accent_dark), PorterDuff.Mode.SRC_ATOP); + } + format = context.getString(R.string.notification_reblog_format); break; } } + message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); String wholeMessage = String.format(format, displayName); final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), @@ -338,12 +396,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter { boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); contentWarningBar.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); - setupContentAndSpoiler(false); + setupContentAndSpoiler(notificationViewData, listener); } void setupButtons(final NotificationActionListener listener, final String accountId, final String notificationId) { - this.listener = listener; + this.notificationActionListener = listener; this.accountId = accountId; this.notificationId = notificationId; } @@ -362,11 +420,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } if (notificationAvatarUrl == null || notificationAvatarUrl.isEmpty()) { - notificationAvatar.setVisibility(View.GONE); + notificationAvatar.setImageResource(R.drawable.avatar_default); } else { - notificationAvatar.setVisibility(View.VISIBLE); Picasso.with(context) .load(notificationAvatarUrl) + .placeholder(R.drawable.avatar_default) .fit() .transform(new RoundedTransformation(7, 0)) .into(notificationAvatar); @@ -377,46 +435,48 @@ public class NotificationsAdapter extends RecyclerView.Adapter { public void onClick(View v) { switch (v.getId()) { case R.id.notification_container: - if (listener != null) listener.onViewStatusForNotificationId(notificationId); + case R.id.notification_content: + if (notificationActionListener != null) notificationActionListener.onViewStatusForNotificationId(notificationId); break; - case R.id.notification_top_bar: - if (listener != null) listener.onViewAccount(accountId); + case R.id.notification_top_text: + if (notificationActionListener != null) notificationActionListener.onViewAccount(accountId); break; } } - private void setupContentAndSpoiler(boolean shouldShowContentIfSpoiler) { + private void setupContentAndSpoiler(NotificationViewData.Concrete notificationViewData, final LinkListener listener) { + + boolean shouldShowContentIfSpoiler = notificationViewData.isExpanded(); boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); - CharSequence content; if (!shouldShowContentIfSpoiler && hasSpoiler) { - if (statusViewData.getMentions() != null && - statusViewData.getMentions().length > 0) { - // If there is a content warning and mentions we're alternating between - // showing mentions and showing full content. As mentions are plain text we - // have to construct URLSpans ourselves. - SpannableStringBuilder contentBuilder = new SpannableStringBuilder(); - for (Status.Mention mention : statusViewData.getMentions()) { - int start = contentBuilder.length() > 0 ? contentBuilder.length() - 1 : 0; - contentBuilder.append('@'); - contentBuilder.append(mention.username); - contentBuilder.append(' '); - contentBuilder.setSpan(new URLSpan(mention.url), start, - mention.username.length() + 1, 0); - } - content = contentBuilder; - } else { - content = null; - } + statusContent.setVisibility(View.GONE); } else { - content = statusViewData.getContent(); + statusContent.setVisibility(View.VISIBLE); } - statusContent.setText(content); - contentWarningDescriptionTextView.setText(statusViewData.getSpoilerText()); + + Spanned content = statusViewData.getContent(); + List emojis = statusViewData.getEmojis(); + + Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent); + + LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); + + + Spanned emojifiedContentWarning = + CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getEmojis(), contentWarningDescriptionTextView); + contentWarningDescriptionTextView.setText(emojifiedContentWarning); } @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - setupContentAndSpoiler(isChecked); + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + notificationActionListener.onExpandedChange(isChecked, getAdapterPosition()); + } + if (isChecked) { + statusContent.setVisibility(View.VISIBLE); + } else { + statusContent.setVisibility(View.GONE); + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 2bb0594b3..e45f5cbd9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1,21 +1,14 @@ package com.keylesspalace.tusky.adapter; import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Paint; -import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; -import android.preference.PreferenceManager; import android.support.annotation.DrawableRes; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.content.res.AppCompatResources; import android.support.v7.widget.RecyclerView; -import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.text.style.ReplacementSpan; +import android.text.TextUtils; import android.view.View; import android.widget.CompoundButton; import android.widget.ImageButton; @@ -24,26 +17,24 @@ import android.widget.TextView; import android.widget.ToggleButton; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.DateUtils; +import com.keylesspalace.tusky.util.HtmlUtils; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.view.RoundedTransformation; import com.keylesspalace.tusky.viewdata.StatusViewData; -import com.squareup.picasso.Callback; import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; import com.varunest.sparkbutton.SparkButton; import com.varunest.sparkbutton.SparkEventListener; -import java.lang.ref.WeakReference; import java.util.Date; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -class StatusBaseViewHolder extends RecyclerView.ViewHolder { +abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private View container; private TextView displayName; private TextView username; @@ -58,23 +49,26 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { private ImageView mediaPreview1; private ImageView mediaPreview2; private ImageView mediaPreview3; - private View sensitiveMediaWarning; + private ImageView mediaOverlay0; + private ImageView mediaOverlay1; + private ImageView mediaOverlay2; + private ImageView mediaOverlay3; + private TextView sensitiveMediaWarning; private View sensitiveMediaShow; - private View videoIndicator; private TextView mediaLabel; private View contentWarningBar; private TextView contentWarningDescription; private ToggleButton contentWarningButton; ImageView avatar; - TextView timestamp; + TextView timestampInfo; StatusBaseViewHolder(View itemView) { super(itemView); container = itemView.findViewById(R.id.status_container); displayName = itemView.findViewById(R.id.status_display_name); username = itemView.findViewById(R.id.status_username); - timestamp = itemView.findViewById(R.id.status_timestamp); + timestampInfo = itemView.findViewById(R.id.status_timestamp_info); content = itemView.findViewById(R.id.status_content); avatar = itemView.findViewById(R.id.status_avatar); replyButton = itemView.findViewById(R.id.status_reply); @@ -87,15 +81,20 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { mediaPreview1 = itemView.findViewById(R.id.status_media_preview_1); mediaPreview2 = itemView.findViewById(R.id.status_media_preview_2); mediaPreview3 = itemView.findViewById(R.id.status_media_preview_3); + mediaOverlay0 = itemView.findViewById(R.id.status_media_overlay_0); + mediaOverlay1 = itemView.findViewById(R.id.status_media_overlay_1); + mediaOverlay2 = itemView.findViewById(R.id.status_media_overlay_2); + mediaOverlay3 = itemView.findViewById(R.id.status_media_overlay_3); sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button); - videoIndicator = itemView.findViewById(R.id.status_video_indicator); mediaLabel = itemView.findViewById(R.id.status_media_label); contentWarningBar = itemView.findViewById(R.id.status_content_warning_bar); contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description); contentWarningButton = itemView.findViewById(R.id.status_content_warning_button); } + protected abstract int getMediaPreviewHeight(Context context); + private void setDisplayName(String name) { displayName.setText(name); } @@ -107,44 +106,11 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { username.setText(usernameText); } - private Callback spanCallback = new Callback() { - @Override - public void onSuccess() { - content.invalidate(); - } - - @Override - public void onError() { - } - }; - private void setContent(Spanned content, Status.Mention[] mentions, List emojis, StatusActionListener listener) { - Context context = this.content.getContext(); - SpannableStringBuilder builder = new SpannableStringBuilder(content); - if (!emojis.isEmpty()) { - CharSequence text = builder.subSequence(0, builder.length()); - for (Status.Emoji emoji : emojis) { - CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':'); - Matcher matcher = Pattern.compile(pattern.toString()).matcher(text); - while (matcher.find()) { - // We keep a span as a Picasso target, because Picasso keeps weak reference to - // the target so an anonymous class would likely be garbage collected. - EmojiSpan span = new EmojiSpan(context); - span.setCallback(spanCallback); - builder.setSpan(span, matcher.start(), matcher.end(), 0); - Picasso.with(container.getContext()) - .load(emoji.getUrl()) - .into(span); - } - } - } + Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content); - /* Redirect URLSpan's in the status content to the listener for viewing tag pages and - * account pages. */ - boolean useCustomTabs = - PreferenceManager.getDefaultSharedPreferences(context).getBoolean("customTabs", false); - LinkHelper.setClickableText(this.content, builder, mentions, useCustomTabs, listener); + LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); } void setAvatar(String url, @Nullable String rebloggedUrl) { @@ -160,7 +126,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { } protected void setCreatedAt(@Nullable Date createdAt) { - // This is the visible timestamp. + // This is the visible timestampInfo. String readout; /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" * as 17 meters instead of minutes. */ @@ -168,7 +134,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (createdAt != null) { long then = createdAt.getTime(); long now = new Date().getTime(); - readout = DateUtils.getRelativeTimeSpanString(timestamp.getContext(), then, now); + readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now); readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now, android.text.format.DateUtils.SECOND_IN_MILLIS, android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE); @@ -177,8 +143,18 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { readout = "?m"; readoutAloud = "? minutes"; } - timestamp.setText(readout); - timestamp.setContentDescription(readoutAloud); + timestampInfo.setText(readout); + timestampInfo.setContentDescription(readoutAloud); + } + + + private void setIsReply(boolean isReply) { + if(isReply) { + replyButton.setImageResource(R.drawable.ic_reply_all_24dp); + } else { + replyButton.setImageResource(R.drawable.ic_reply_24dp); + } + } private void setReblogged(boolean reblogged) { @@ -214,11 +190,14 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { favouriteButton.setChecked(favourited); } - private void setMediaPreviews(final Status.MediaAttachment[] attachments, boolean sensitive, - final StatusActionListener listener, boolean showingSensitive) { + private void setMediaPreviews(final Attachment[] attachments, boolean sensitive, + final StatusActionListener listener, boolean showingContent) { final ImageView[] previews = { mediaPreview0, mediaPreview1, mediaPreview2, mediaPreview3 }; + final ImageView[] overlays = { + mediaOverlay0, mediaOverlay1, mediaOverlay2, mediaOverlay3 + }; Context context = mediaPreview0.getContext(); int mediaPreviewUnloadedId = @@ -234,6 +213,13 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { for (int i = 0; i < n; i++) { String previewUrl = attachments[i].previewUrl; + String description = attachments[i].description; + + if(TextUtils.isEmpty(description)) { + previews[i].setContentDescription(context.getString(R.string.action_view_media)); + } else { + previews[i].setContentDescription(description); + } previews[i].setVisibility(View.VISIBLE); @@ -246,9 +232,11 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { .into(previews[i]); } - final Status.MediaAttachment.Type type = attachments[i].type; - if (type == Status.MediaAttachment.Type.VIDEO | type == Status.MediaAttachment.Type.GIFV) { - videoIndicator.setVisibility(View.VISIBLE); + final Attachment.Type type = attachments[i].type; + if (type == Attachment.Type.VIDEO | type == Attachment.Type.GIFV) { + overlays[i].setVisibility(View.VISIBLE); + } else { + overlays[i].setVisibility(View.GONE); } if (urls[i] == null || urls[i].isEmpty()) { @@ -262,34 +250,56 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { } }); } + + if(n <= 2) { + previews[0].getLayoutParams().height = getMediaPreviewHeight(context)*2; + previews[1].getLayoutParams().height = getMediaPreviewHeight(context)*2; + } else { + previews[0].getLayoutParams().height = getMediaPreviewHeight(context); + previews[1].getLayoutParams().height = getMediaPreviewHeight(context); + previews[2].getLayoutParams().height = getMediaPreviewHeight(context); + previews[3].getLayoutParams().height = getMediaPreviewHeight(context); + } } - SharedPreferences pm = PreferenceManager.getDefaultSharedPreferences(context); - Boolean isAlwayShowSensitive = pm.getBoolean("alwaysShowSensitiveMedia", false); - if (sensitive && (!isAlwayShowSensitive)) { - sensitiveMediaWarning.setVisibility(showingSensitive ? View.GONE : View.VISIBLE); - sensitiveMediaShow.setVisibility(showingSensitive ? View.VISIBLE : View.GONE); - sensitiveMediaShow.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(false, getAdapterPosition()); - } - v.setVisibility(View.GONE); - sensitiveMediaWarning.setVisibility(View.VISIBLE); - } - }); - sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if (getAdapterPosition() != RecyclerView.NO_POSITION) { - listener.onContentHiddenChange(true, getAdapterPosition()); - } - v.setVisibility(View.GONE); - sensitiveMediaShow.setVisibility(View.VISIBLE); - } - }); + + String hiddenContentText; + if(sensitive) { + hiddenContentText = context.getString(R.string.status_sensitive_media_template, + context.getString(R.string.status_sensitive_media_title), + context.getString(R.string.status_sensitive_media_directions)); + + } else { + hiddenContentText = context.getString(R.string.status_sensitive_media_template, + context.getString(R.string.status_media_hidden_title), + context.getString(R.string.status_sensitive_media_directions)); } + sensitiveMediaWarning.setText(HtmlUtils.fromHtml(hiddenContentText)); + + sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); + sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); + sensitiveMediaShow.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(false, getAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaWarning.setVisibility(View.VISIBLE); + } + }); + sensitiveMediaWarning.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (getAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.VISIBLE); + } + }); + + // Hide any of the placeholder previews beyond the ones set. for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) { previews[i].setVisibility(View.GONE); @@ -297,7 +307,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { } @NonNull - private static String getLabelTypeText(Context context, Status.MediaAttachment.Type type) { + private static String getLabelTypeText(Context context, Attachment.Type type) { switch (type) { default: case IMAGE: @@ -309,7 +319,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { } @DrawableRes - private static int getLabelIcon(Status.MediaAttachment.Type type) { + private static int getLabelIcon(Attachment.Type type) { switch (type) { default: case IMAGE: @@ -320,7 +330,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private void setMediaLabel(Status.MediaAttachment[] attachments, boolean sensitive, + private void setMediaLabel(Attachment[] attachments, boolean sensitive, final StatusActionListener listener) { if (attachments.length == 0) { mediaLabel.setVisibility(View.GONE); @@ -349,7 +359,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { for (int i = 0; i < n; i++) { urls[i] = attachments[i].url; } - final Status.MediaAttachment.Type type = attachments[0].type; + final Attachment.Type type = attachments[0].type; mediaLabel.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -363,14 +373,17 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { sensitiveMediaShow.setVisibility(View.GONE); } - private void setSpoilerText(String spoilerText, final boolean expanded, - final StatusActionListener listener) { - contentWarningDescription.setText(spoilerText); + private void setSpoilerText(String spoilerText, List emojis, + final boolean expanded, final StatusActionListener listener) { + CharSequence emojiSpoiler = + CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription); + contentWarningDescription.setText(emojiSpoiler); contentWarningBar.setVisibility(View.VISIBLE); contentWarningButton.setChecked(expanded); contentWarningButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + contentWarningDescription.invalidate(); if (getAdapterPosition() != RecyclerView.NO_POSITION) { listener.onExpandedChange(isChecked, getAdapterPosition()); } @@ -478,21 +491,19 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { setDisplayName(status.getUserFullName()); setUsername(status.getNickname()); setCreatedAt(status.getCreatedAt()); + setIsReply(status.getInReplyToId() != null); setContent(status.getContent(), status.getMentions(), status.getEmojis(), listener); setAvatar(status.getAvatar(), status.getRebloggedAvatar()); setReblogged(status.isReblogged()); setFavourited(status.isFavourited()); - Status.MediaAttachment[] attachments = status.getAttachments(); + Attachment[] attachments = status.getAttachments(); boolean sensitive = status.isSensitive(); if (mediaPreviewEnabled) { - setMediaPreviews(attachments, sensitive, listener, status.isShowingSensitiveContent()); - /* A status without attachments is sometimes still marked sensitive, so it's necessary - * to check both whether there are any attachments and if it's marked sensitive. */ - if (!sensitive || attachments.length == 0) { - hideSensitiveMediaWarning(); - } + setMediaPreviews(attachments, sensitive, listener, status.isShowingContent()); + if (attachments.length == 0) { - videoIndicator.setVisibility(View.GONE); + hideSensitiveMediaWarning(); +// videoIndicator.setVisibility(View.GONE); } // Hide the unused label. mediaLabel.setVisibility(View.GONE); @@ -504,7 +515,7 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { mediaPreview2.setVisibility(View.GONE); mediaPreview3.setVisibility(View.GONE); hideSensitiveMediaWarning(); - videoIndicator.setVisibility(View.GONE); +// videoIndicator.setVisibility(View.GONE); } setupButtons(listener, status.getSenderId()); @@ -512,66 +523,9 @@ class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) { hideSpoilerText(); } else { - setSpoilerText(status.getSpoilerText(), status.isExpanded(), listener); + setSpoilerText(status.getSpoilerText(), status.getEmojis(), status.isExpanded(), listener); } } - private static class EmojiSpan extends ReplacementSpan implements Target { - private @Nullable - Drawable imageDrawable; - private WeakReference callbackWeakReference; - private Context context; - - EmojiSpan(Context context) { - this.context = context.getApplicationContext(); - } - - public void setCallback(Callback callback) { - this.callbackWeakReference = new WeakReference<>(callback); - } - - @Override - public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, - @Nullable Paint.FontMetricsInt fm) { - return (int) (paint.getTextSize()*1.2); - } - - @Override - public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, - int top, int y, int bottom, @NonNull Paint paint) { - if (imageDrawable == null) return; - canvas.save(); - - int emojiSize = (int) (paint.getTextSize() * 1.1); - imageDrawable.setBounds(0, 0, emojiSize, emojiSize); - - int transY = bottom - imageDrawable.getBounds().bottom; - transY -= paint.getFontMetricsInt().descent/2; - canvas.translate(x, transY); - imageDrawable.draw(canvas); - canvas.restore(); - } - - @Override - public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { - // I hope using resources from application context is okay - // It's probably better than keeping activity alive. My assumption is that resources are - // only needed to look up the density which is really unlikely to change with - // configuration - imageDrawable = new BitmapDrawable(context.getResources(), bitmap); - if (callbackWeakReference != null) { - Callback cb = callbackWeakReference.get(); - if (cb != null) cb.onSuccess(); - } - } - - @Override - public void onBitmapFailed(Drawable errorDrawable) { - } - - @Override - public void onPrepareLoad(Drawable placeHolderDrawable) { - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 8461d7760..d31b9d766 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -2,7 +2,6 @@ package com.keylesspalace.tusky.adapter; import android.content.Context; import android.os.Build; -import android.preference.PreferenceManager; import android.support.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; @@ -19,7 +18,7 @@ import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.util.CustomTabURLSpan; +import com.keylesspalace.tusky.util.CustomURLSpan; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.viewdata.StatusViewData; import com.squareup.picasso.Picasso; @@ -30,7 +29,6 @@ import java.util.Date; class StatusDetailedViewHolder extends StatusBaseViewHolder { private TextView reblogs; private TextView favourites; - private TextView application; private LinearLayout cardView; private LinearLayout cardInfo; private ImageView cardImage; @@ -42,7 +40,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { super(view); reblogs = view.findViewById(R.id.status_reblogs); favourites = view.findViewById(R.id.status_favourites); - 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); @@ -51,36 +48,36 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { cardUrl = view.findViewById(R.id.card_link); } + @Override + protected int getMediaPreviewHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.status_detail_media_preview_height); + } + @Override protected void setCreatedAt(@Nullable Date createdAt) { if (createdAt != null) { - DateFormat dateFormat = android.text.format.DateFormat.getMediumDateFormat( - timestamp.getContext()); - timestamp.setText(dateFormat.format(createdAt)); + DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT); + timestampInfo.setText(dateFormat.format(createdAt)); } else { - timestamp.setText(""); + timestampInfo.setText(""); } } private void setApplication(@Nullable Status.Application app) { - if (app == null) { - application.setText(""); - } else if (app.website != null) { - URLSpan span; - Context context = application.getContext(); - boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context) - .getBoolean("customTabs", true); - if (useCustomTabs) { - span = new CustomTabURLSpan(app.website); + if (app != null) { + + timestampInfo.append(" • "); + + if (app.website != null) { + URLSpan span = new CustomURLSpan(app.website); + + SpannableStringBuilder text = new SpannableStringBuilder(app.name); + text.setSpan(span, 0, app.name.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); + timestampInfo.append(text); + timestampInfo.setMovementMethod(LinkMovementMethod.getInstance()); } else { - span = new URLSpan(app.website); + timestampInfo.append(app.name); } - SpannableStringBuilder text = new SpannableStringBuilder(app.name); - text.setSpan(span, 0, app.name.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); - application.setText(text); - application.setMovementMethod(LinkMovementMethod.getInstance()); - } else { - application.setText(app.name); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 943e7d729..3e45e0019 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -31,14 +31,12 @@ import com.varunest.sparkbutton.helpers.Utils; public class StatusViewHolder extends StatusBaseViewHolder { private ImageView avatarReblog; - private View rebloggedBar; - private TextView rebloggedByDisplayName; + private TextView rebloggedBar; StatusViewHolder(View itemView) { super(itemView); avatarReblog = itemView.findViewById(R.id.status_avatar_reblog); - rebloggedBar = itemView.findViewById(R.id.status_reblogged_bar); - rebloggedByDisplayName = itemView.findViewById(R.id.status_reblogged); + rebloggedBar = itemView.findViewById(R.id.status_reblogged); } @Override @@ -66,6 +64,11 @@ public class StatusViewHolder extends StatusBaseViewHolder { } } + @Override + protected int getMediaPreviewHeight(Context context) { + return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); + } + @Override void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener, boolean mediaPreviewEnabled) { @@ -90,10 +93,10 @@ public class StatusViewHolder extends StatusBaseViewHolder { } private void setRebloggedByDisplayName(String name) { - Context context = rebloggedByDisplayName.getContext(); + Context context = rebloggedBar.getContext(); String format = context.getString(R.string.status_boosted_format); String boostedText = String.format(format, name); - rebloggedByDisplayName.setText(boostedText); + rebloggedBar.setText(boostedText); rebloggedBar.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java index c743a54b4..63757e8f4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.java @@ -22,7 +22,6 @@ import android.view.View; import android.view.ViewGroup; import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -38,8 +37,6 @@ public class ThreadAdapter extends RecyclerView.Adapter { private boolean mediaPreviewEnabled; private int detailedStatusPosition; - private Card detailedStatusCard; - public ThreadAdapter(StatusActionListener listener) { this.statusActionListener = listener; this.statuses = new ArrayList<>(); @@ -155,4 +152,8 @@ public class ThreadAdapter extends RecyclerView.Adapter { detailedStatusPosition = position; } } + + public int getDetailedStatusPosition() { + return detailedStatusPosition; + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.java b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.java new file mode 100644 index 000000000..355f03c2a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.java @@ -0,0 +1,79 @@ +/* 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 . */ + +package com.keylesspalace.tusky.entity; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; + +public class Attachment { + public String id; + + public String url; + + @SerializedName("preview_url") + public String previewUrl; + + @SerializedName("text_url") + public String textUrl; + + public Type type; + + public String description; + + public static class Meta { + public MediaProperties original; + public MediaProperties small; + } + + public static class MediaProperties { + public int width; + public int height; + public float aspect; + } + + @JsonAdapter(MediaTypeDeserializer.class) + public enum Type { + @SerializedName("image") + IMAGE, + @SerializedName("gifv") + GIFV, + @SerializedName("video") + VIDEO, + @SerializedName("unknown") + UNKNOWN + } + + static class MediaTypeDeserializer implements JsonDeserializer { + @Override + public Type deserialize(JsonElement json, java.lang.reflect.Type classOfT, JsonDeserializationContext context) + throws JsonParseException { + switch(json.toString()) { + case "\"image\"": + return Type.IMAGE; + case "\"gifv\"": + return Type.GIFV; + case "\"video\"": + return Type.VIDEO; + default: + return Type.UNKNOWN; + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java b/app/src/main/java/com/keylesspalace/tusky/entity/Media.java deleted file mode 100644 index 82760886e..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Media.java +++ /dev/null @@ -1,32 +0,0 @@ -/* 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 . */ - -package com.keylesspalace.tusky.entity; - -import com.google.gson.annotations.SerializedName; - -public class Media { - public String id; - - public String type; - - public String url; - - @SerializedName("preview_url") - public String previewUrl; - - @SerializedName("text_url") - public String textUrl; -} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java index 7f9771eed..1e7ef472d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.java +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.java @@ -17,10 +17,6 @@ package com.keylesspalace.tusky.entity; import android.text.Spanned; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import java.util.Date; @@ -137,7 +133,7 @@ public class Status { } @SerializedName("media_attachments") - public MediaAttachment[] attachments; + public Attachment[] attachments; public Mention[] mentions; @@ -159,48 +155,6 @@ public class Status { return id != null ? id.hashCode() : 0; } - public static class MediaAttachment { - @com.google.gson.annotations.JsonAdapter(MediaTypeDeserializer.class) - public enum Type { - @SerializedName("image") - IMAGE, - @SerializedName("gifv") - GIFV, - @SerializedName("video") - VIDEO, - UNKNOWN - } - - public String url; - - @SerializedName("preview_url") - public String previewUrl; - - @SerializedName("text_url") - public String textUrl; - - @SerializedName("remote_url") - public String remoteUrl; - - public Type type; - - static class MediaTypeDeserializer implements JsonDeserializer { - @Override - public Type deserialize(JsonElement json, java.lang.reflect.Type classOfT, JsonDeserializationContext context) - throws JsonParseException { - switch(json.toString()) { - case "\"image\"": - return Type.IMAGE; - case "\"gifv\"": - return Type.GIFV; - case "\"video\"": - return Type.VIDEO; - default: - return Type.UNKNOWN; - } - } - } - } public static final class Mention { public String id; diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index 6e18fa4cb..22abcca38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -34,6 +34,7 @@ import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.ViewVideoActivity +import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.view.SquareImageView @@ -86,7 +87,7 @@ class AccountMediaFragment : BaseFragment() { body?.let { fetched -> statuses.addAll(0, fetched) // flatMap requires iterable but I don't want to box each array into list - val result = mutableListOf() + val result = mutableListOf() for (status in fetched) { result.addAll(status.attachments) } @@ -110,7 +111,7 @@ class AccountMediaFragment : BaseFragment() { statuses.addAll(fetched) Log.d(TAG, "now there are ${statuses.size} statuses") // flatMap requires iterable but I don't want to box each array into list - val result = mutableListOf() + val result = mutableListOf() for (status in fetched) { result.addAll(status.attachments) } @@ -190,12 +191,12 @@ class AccountMediaFragment : BaseFragment() { } } - private fun viewMedia(items: List, currentIndex: Int, view: View?) { + private fun viewMedia(items: List, currentIndex: Int, view: View?) { val urls = items.map { it.url }.toTypedArray() val type = items[currentIndex].type when (type) { - Status.MediaAttachment.Type.IMAGE -> { + Attachment.Type.IMAGE -> { val intent = Intent(context, ViewMediaActivity::class.java) intent.putExtra("urls", urls) intent.putExtra("urlIndex", currentIndex) @@ -208,12 +209,12 @@ class AccountMediaFragment : BaseFragment() { startActivity(intent) } } - Status.MediaAttachment.Type.GIFV, Status.MediaAttachment.Type.VIDEO -> { + Attachment.Type.GIFV, Attachment.Type.VIDEO -> { val intent = Intent(context, ViewVideoActivity::class.java) intent.putExtra("url", urls[currentIndex]) startActivity(intent) } - Status.MediaAttachment.Type.UNKNOWN, null -> { + Attachment.Type.UNKNOWN, null -> { }/* Intentionally do nothing. This case is here is to handle when new attachment * types are added to the API before code is added here to handle them. So, the * best fallback is to just show the preview and ignore requests to view them. */ @@ -229,16 +230,16 @@ class AccountMediaFragment : BaseFragment() { var baseItemColor = Color.BLACK - private val items = mutableListOf() + private val items = mutableListOf() private val itemBgBaseHSV = FloatArray(3) private val random = Random() - fun addTop(newItems: List) { + fun addTop(newItems: List) { items.addAll(0, newItems) notifyItemRangeInserted(0, newItems.size) } - fun addBottom(newItems: List) { + fun addBottom(newItems: List) { if (newItems.isEmpty()) return val oldLen = items.size diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 1ca8a19a7..772467f13 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -42,6 +42,7 @@ import com.keylesspalace.tusky.NotificationPullJobCreator; import com.keylesspalace.tusky.adapter.FooterViewHolder; import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.ActionButtonActivity; @@ -108,6 +109,7 @@ public class NotificationsFragment extends SFragment implements private int bottomFetches; private String bottomId; private String topId; + private boolean alwaysShowSensitiveMedia; // Each element is either a Notification for loading data or a Placeholder private final PairedList, NotificationViewData> notifications @@ -116,7 +118,7 @@ public class NotificationsFragment extends SFragment implements public NotificationViewData apply(Either input) { if (input.isRight()) { Notification notification = input.getAsRight(); - return ViewDataUtils.notificationToViewData(notification); + return ViewDataUtils.notificationToViewData(notification, alwaysShowSensitiveMedia); } else { return new NotificationViewData.Placeholder(false); } @@ -155,6 +157,7 @@ public class NotificationsFragment extends SFragment implements adapter = new NotificationsAdapter(this, this); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( getActivity()); + alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false); boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true); adapter.setMediaPreviewEnabled(mediaPreviewEnabled); recyclerView.setAdapter(adapter); @@ -273,13 +276,19 @@ public class NotificationsFragment extends SFragment implements if (status.reblog != null) { status.reblog.reblogged = reblog; } - // Java's type inference *eyeroll* - notifications.set(position, - Either.right(notification)); - adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true); + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete)notifications.getPairedItem(position); - adapter.notifyItemChanged(position); + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setReblogged(reblog); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.isExpanded()); + + notifications.setPairedItem(position, newViewData); + + adapter.updateItemWithNotify(position, newViewData, true); } } @@ -305,12 +314,19 @@ public class NotificationsFragment extends SFragment implements status.reblog.favourited = favourite; } - notifications.set(position, - Either.right(notification)); + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete)notifications.getPairedItem(position); - adapter.updateItemWithNotify(position, notifications.getPairedItem(position), true); + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setFavourited(favourite); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.isExpanded()); + + notifications.setPairedItem(position, newViewData); + + adapter.updateItemWithNotify(position, newViewData, true); - adapter.notifyItemChanged(position); } } @@ -328,7 +344,7 @@ public class NotificationsFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, View view) { super.viewMedia(urls, urlIndex, type, view); } @@ -354,7 +370,7 @@ public class NotificationsFragment extends SFragment implements .setIsExpanded(expanded) .createStatusViewData(); NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), - old.getId(), old.getAccount(), statusViewData); + old.getId(), old.getAccount(), statusViewData, expanded); notifications.setPairedItem(position, notificationViewData); adapter.updateItemWithNotify(position, notificationViewData, false); } @@ -368,7 +384,7 @@ public class NotificationsFragment extends SFragment implements .setIsShowingSensitiveContent(isShowing) .createStatusViewData(); NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), - old.getId(), old.getAccount(), statusViewData); + old.getId(), old.getAccount(), statusViewData, old.isExpanded()); notifications.setPairedItem(position, notificationViewData); adapter.updateItemWithNotify(position, notificationViewData, false); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index eb20619b5..7a9a96ec0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.ViewThreadActivity; import com.keylesspalace.tusky.ViewVideoActivity; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Relationship; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.AdapterItemRemover; @@ -259,7 +260,7 @@ public abstract class SFragment extends BaseFragment implements AdapterItemRemov popup.show(); } - protected void viewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + protected void viewMedia(String[] urls, int urlIndex, Attachment.Type type, @Nullable View view) { switch (type) { case IMAGE: { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 26c0df191..f3294d58e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.FooterViewHolder; import com.keylesspalace.tusky.adapter.TimelineAdapter; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.StatusActionListener; @@ -108,13 +109,16 @@ public class TimelineFragment extends SFragment implements private String bottomId; @Nullable private String topId; + + private boolean alwaysShowSensitiveMedia; + private PairedList, StatusViewData> statuses = new PairedList<>(new Function, StatusViewData>() { @Override public StatusViewData apply(Either input) { Status status = input.getAsRightOrNull(); if (status != null) { - return ViewDataUtils.statusToViewData(status); + return ViewDataUtils.statusToViewData(status, alwaysShowSensitiveMedia); } else { return new StatusViewData.Placeholder(false); } @@ -150,7 +154,7 @@ public class TimelineFragment extends SFragment implements } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Bundle arguments = getArguments(); kind = Kind.valueOf(arguments.getString(KIND_ARG)); @@ -179,6 +183,7 @@ public class TimelineFragment extends SFragment implements SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( getActivity()); preferences.registerOnSharedPreferenceChangeListener(this); + alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false); boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true); adapter.setMediaPreviewEnabled(mediaPreviewEnabled); recyclerView.setAdapter(adapter); @@ -402,7 +407,7 @@ public class TimelineFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, View view) { super.viewMedia(urls, urlIndex, type, view); } @@ -462,6 +467,10 @@ public class TimelineFragment extends SFragment implements } break; } + case "alwaysShowSensitiveMedia": { + //it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = sharedPreferences.getBoolean("alwaysShowSensitiveMedia", false); + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index fc527cbcb..80b0e0e6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.fragment; +import android.arch.core.util.Function; import android.content.Context; import android.content.SharedPreferences; import android.graphics.drawable.Drawable; @@ -23,7 +24,6 @@ import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.design.widget.Snackbar; -import android.support.v4.content.ContextCompat; import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.widget.DividerItemDecoration; @@ -37,6 +37,7 @@ import android.view.ViewGroup; import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.ThreadAdapter; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.entity.StatusContext; @@ -66,11 +67,17 @@ public class ViewThreadFragment extends SFragment implements private String thisThreadsStatusId; private TimelineReceiver timelineReceiver; private Card card; + private boolean alwaysShowSensitiveMedia; private int statusIndex = 0; - private final PairedList statuses = - new PairedList<>(ViewDataUtils.statusMapper()); + private PairedList statuses = + new PairedList<>(new Function() { + @Override + public StatusViewData.Concrete apply(Status input) { + return ViewDataUtils.statusToViewData(input, alwaysShowSensitiveMedia); + } + }); public static ViewThreadFragment newInstance(String id) { Bundle arguments = new Bundle(); @@ -82,7 +89,7 @@ public class ViewThreadFragment extends SFragment implements @Nullable @Override - public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); @@ -96,15 +103,19 @@ public class ViewThreadFragment extends SFragment implements recyclerView.setLayoutManager(layoutManager); DividerItemDecoration divider = new DividerItemDecoration( context, layoutManager.getOrientation()); - Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, + Drawable dividerDrawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, R.drawable.status_divider_dark); - divider.setDrawable(drawable); + divider.setDrawable(dividerDrawable); recyclerView.addItemDecoration(divider); + + Drawable threadLineDrawable = ThemeUtils.getDrawable(context, R.attr.conversation_thread_line_drawable, + R.drawable.conversation_thread_line_dark); recyclerView.addItemDecoration(new ConversationLineItemDecoration(context, - ContextCompat.getDrawable(context, R.drawable.conversation_divider_dark))); + threadLineDrawable)); adapter = new ThreadAdapter(this); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences( getActivity()); + alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false); boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true); adapter.setMediaPreviewEnabled(mediaPreviewEnabled); recyclerView.setAdapter(adapter); @@ -157,10 +168,16 @@ public class ViewThreadFragment extends SFragment implements if (status.reblog != null) { status.reblog.reblogged = reblog; } - // create new viewData as side effect - statuses.set(position, status); - adapter.setItem(position, statuses.getPairedItem(position), true); + StatusViewData.Concrete viewdata = statuses.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setReblogged(reblog); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); } } @@ -184,9 +201,16 @@ public class ViewThreadFragment extends SFragment implements if (status.reblog != null) { status.reblog.favourited = favourite; } - // create new viewData as side effect - statuses.set(position, status); - adapter.setItem(position, statuses.getPairedItem(position), true); + + StatusViewData.Concrete viewdata = statuses.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata)); + viewDataBuilder.setFavourited(favourite); + + StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData(); + + statuses.setPairedItem(position, newViewData); + adapter.setItem(position, newViewData, true); } } @@ -204,7 +228,7 @@ public class ViewThreadFragment extends SFragment implements } @Override - public void onViewMedia(String[] urls, int urlIndex, Status.MediaAttachment.Type type, + public void onViewMedia(String[] urls, int urlIndex, Attachment.Type type, View view) { super.viewMedia(urls, urlIndex, type, view); } @@ -367,6 +391,7 @@ public class ViewThreadFragment extends SFragment implements public void onClick(View v) { sendThreadRequest(id); sendStatusRequest(id); + sendCardRequest(id); } }) .show(); @@ -392,6 +417,7 @@ public class ViewThreadFragment extends SFragment implements .setCard(card) .createStatusViewData(); } + statuses.setPairedItem(i, viewData); adapter.addItem(i, viewData); return i; } @@ -432,6 +458,8 @@ public class ViewThreadFragment extends SFragment implements viewData = new StatusViewData.Builder(viewData) .setCard(card) .createStatusViewData(); + statuses.setPairedItem(statusIndex, viewData); + } adapter.addItem(statusIndex, viewData); } diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index a8ff8db17..4a5d7b5c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -17,14 +17,14 @@ package com.keylesspalace.tusky.interfaces; import android.view.View; -import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.Attachment; public interface StatusActionListener extends LinkListener { void onReply(int position); void onReblog(final boolean reblog, final int position); void onFavourite(final boolean favourite, final int position); void onMore(View view, final int position); - void onViewMedia(String[] urls, int index, Status.MediaAttachment.Type type, View view); + void onViewMedia(String[] urls, int index, Attachment.Type type, View view); void onViewThread(int position); void onOpenReblog(int position); void onExpandedChange(boolean expanded, int position); diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java index 502eeb39f..c37bb4eb5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.java @@ -20,8 +20,8 @@ import android.support.annotation.Nullable; import com.keylesspalace.tusky.entity.AccessToken; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.AppCredentials; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; -import com.keylesspalace.tusky.entity.Media; import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Profile; import com.keylesspalace.tusky.entity.Relationship; @@ -80,7 +80,7 @@ public interface MastodonApi { @Multipart @POST("api/v1/media") - Call uploadMedia(@Part MultipartBody.Part file); + Call uploadMedia(@Part MultipartBody.Part file); @FormUrlEncoded @POST("api/v1/statuses") diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java new file mode 100644 index 000000000..03208429f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.java @@ -0,0 +1,137 @@ +/* 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 . */ + +package com.keylesspalace.tusky.util; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.style.ReplacementSpan; +import android.widget.TextView; + +import com.keylesspalace.tusky.entity.Status; +import com.squareup.picasso.Picasso; +import com.squareup.picasso.Target; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class CustomEmojiHelper { + + /** + * replaces emoji shortcodes in a text with EmojiSpans + * @param text the text containing custom emojis + * @param emojis a list of the custom emojis + * @param textView a reference to the textView the emojis will be shown in + * @return the text with the shortcodes replaced by EmojiSpans + */ + public static Spanned emojifyText(Spanned text, List emojis, final TextView textView) { + + if (!emojis.isEmpty()) { + + SpannableStringBuilder builder = new SpannableStringBuilder(text); + for (Status.Emoji emoji : emojis) { + CharSequence pattern = new StringBuilder(":").append(emoji.getShortcode()).append(':'); + Matcher matcher = Pattern.compile(pattern.toString()).matcher(text); + while (matcher.find()) { + // We keep a span as a Picasso target, because Picasso keeps weak reference to + // the target so an anonymous class would likely be garbage collected. + EmojiSpan span = new EmojiSpan(textView); + builder.setSpan(span, matcher.start(), matcher.end(), 0); + Picasso.with(textView.getContext()) + .load(emoji.getUrl()) + .into(span); + } + } + + return builder; + } + + return text; + } + + public static Spanned emojifyString(String string, List emojis, final TextView textView) { + return emojifyText(new SpannedString(string), emojis, textView); + } + + + public static class EmojiSpan extends ReplacementSpan implements Target { + + private @Nullable Drawable imageDrawable; + private WeakReference textViewWeakReference; + + EmojiSpan(TextView textView) { + this.textViewWeakReference = new WeakReference<>(textView); + } + + @Override + public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, + @Nullable Paint.FontMetricsInt fm) { + + /* update FontMetricsInt or otherwise span does not get drawn when + it covers the whole text */ + Paint.FontMetricsInt metrics = paint.getFontMetricsInt(); + if (fm != null) { + fm.top = metrics.top; + fm.ascent = metrics.ascent; + fm.descent = metrics.descent; + fm.bottom = metrics.bottom; + } + + return (int) (paint.getTextSize()*1.2); + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, + int top, int y, int bottom, @NonNull Paint paint) { + if (imageDrawable == null) return; + canvas.save(); + + int emojiSize = (int) (paint.getTextSize() * 1.1); + imageDrawable.setBounds(0, 0, emojiSize, emojiSize); + + int transY = bottom - imageDrawable.getBounds().bottom; + transY -= paint.getFontMetricsInt().descent/2; + canvas.translate(x, transY); + imageDrawable.draw(canvas); + canvas.restore(); + } + + @Override + public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { + TextView textView = textViewWeakReference.get(); + if(textView != null) { + imageDrawable = new BitmapDrawable(textView.getContext().getResources(), bitmap); + textView.invalidate(); + } + } + + @Override + public void onBitmapFailed(Drawable errorDrawable) {} + + @Override + public void onPrepareLoad(Drawable placeHolderDrawable) {} + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java deleted file mode 100644 index 2a25120b6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomTabURLSpan.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.keylesspalace.tusky.util; - -import android.net.Uri; -import android.os.Parcel; -import android.os.Parcelable; -import android.text.style.URLSpan; -import android.view.View; - -public class CustomTabURLSpan extends URLSpan { - public CustomTabURLSpan(String url) { - super(url); - } - - private CustomTabURLSpan(Parcel src) { - super(src); - } - - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - - @Override - public CustomTabURLSpan createFromParcel(Parcel source) { - return new CustomTabURLSpan(source); - } - - @Override - public CustomTabURLSpan[] newArray(int size) { - return new CustomTabURLSpan[size]; - } - }; - - @Override - public void onClick(View view) { - Uri uri = Uri.parse(getURL()); - LinkHelper.openLinkInCustomTab(uri, view.getContext()); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java b/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java new file mode 100644 index 000000000..e772162e5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomURLSpan.java @@ -0,0 +1,41 @@ +package com.keylesspalace.tusky.util; + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextPaint; +import android.text.style.URLSpan; +import android.view.View; + +public class CustomURLSpan extends URLSpan { + public CustomURLSpan(String url) { + super(url); + } + + private CustomURLSpan(Parcel src) { + super(src); + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + @Override + public CustomURLSpan createFromParcel(Parcel source) { + return new CustomURLSpan(source); + } + + @Override + public CustomURLSpan[] newArray(int size) { + return new CustomURLSpan[size]; + } + + }; + + @Override + public void onClick(View view) { + LinkHelper.openLink(getURL(), view.getContext()); + } + + @Override public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 19efce4ed..27de3f403 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -26,6 +26,7 @@ import android.support.customtabs.CustomTabsIntent; import android.support.v4.content.ContextCompat; import android.text.SpannableStringBuilder; import android.text.Spanned; +import android.text.TextPaint; import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; @@ -63,12 +64,11 @@ public class LinkHelper { * @param view the returned text will be put in * @param content containing text with mentions, links, or hashtags * @param mentions any '@' mentions which are known to be in the content - * @param useCustomTabs whether to use custom tabs when opening web links * @param listener to notify about particular spans that are clicked */ public static void setClickableText(TextView view, Spanned content, - @Nullable Status.Mention[] mentions, boolean useCustomTabs, - final LinkListener listener) { + @Nullable Status.Mention[] mentions, final LinkListener listener) { + SpannableStringBuilder builder = new SpannableStringBuilder(content); URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class); for (URLSpan span : urlSpans) { @@ -83,17 +83,21 @@ public class LinkHelper { public void onClick(View widget) { 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) { + } else if (text.charAt(0) == '@' && mentions != null && mentions.length > 0) { String accountUsername = text.subSequence(1, text.length()).toString(); /* There may be multiple matches for users on different instances with the same * username. If a match has the same domain we know it's for sure the same, but if * that can't be found then just go with whichever one matched last. */ String id = null; for (Status.Mention mention : mentions) { - if (mention.localUsername.equals(accountUsername)) { + if (mention.localUsername.equalsIgnoreCase(accountUsername)) { id = mention.id; if (mention.url.contains(getDomain(span.getURL()))) { break; @@ -107,12 +111,16 @@ public class LinkHelper { public void onClick(View widget) { listener.onViewAccount(accountId); } + @Override public void updateDrawState(TextPaint ds) { + super.updateDrawState(ds); + ds.setUnderlineText(false); + } }; builder.removeSpan(span); builder.setSpan(newSpan, start, end, flags); } - } else if (useCustomTabs) { - ClickableSpan newSpan = new CustomTabURLSpan(span.getURL()); + } else { + ClickableSpan newSpan = new CustomURLSpan(span.getURL()); builder.removeSpan(span); builder.setSpan(newSpan, start, end, flags); } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java index cb9426a25..9f4fb4367 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.java @@ -85,9 +85,7 @@ public class OkHttpUtils { @NonNull public static OkHttpClient getCompatibleClient() { - OkHttpClient client = getCompatibleClientBuilder().build(); - Log.d(TAG, client.connectTimeoutMillis()+" "+client.readTimeoutMillis()+" "+client.writeTimeoutMillis()); - return client; + return getCompatibleClientBuilder().build(); } /** diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 502d558c3..f3cfa7087 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.util; -import android.arch.core.util.Function; import android.support.annotation.Nullable; import com.keylesspalace.tusky.entity.Notification; @@ -23,16 +22,14 @@ import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; -import java.util.ArrayList; -import java.util.List; - /** * Created by charlag on 12/07/2017. */ public final class ViewDataUtils { @Nullable - public static StatusViewData.Concrete statusToViewData(@Nullable Status status) { + public static StatusViewData.Concrete statusToViewData(@Nullable Status status, + boolean alwaysShowSensitiveMedia) { if (status == null) return null; Status visibleStatus = status.reblog == null ? status : status.reblog; return new StatusViewData.Builder().setId(status.id) @@ -51,6 +48,7 @@ public final class ViewDataUtils { .setNickname(visibleStatus.account.username) .setRebloggedAvatar(status.reblog == null ? null : status.account.avatar) .setSensitive(visibleStatus.sensitive) + .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.sensitive) .setSpoilerText(visibleStatus.spoilerText) .setRebloggedByUsername(status.reblog == null ? null : status.account.username) .setUserFullName(visibleStatus.account.getDisplayName()) @@ -62,37 +60,9 @@ public final class ViewDataUtils { .createStatusViewData(); } - public static List statusListToViewDataList(List statuses) { - List viewDatas = new ArrayList<>(statuses.size()); - for (Status s : statuses) { - viewDatas.add(statusToViewData(s)); - } - return viewDatas; - } - - public static Function statusMapper() { - return statusMapper; - } - - public static NotificationViewData.Concrete notificationToViewData(Notification notification) { + public static NotificationViewData.Concrete notificationToViewData(Notification notification, boolean alwaysShowSensitiveData) { return new NotificationViewData.Concrete(notification.type, notification.id, notification.account, - statusToViewData(notification.status)); + statusToViewData(notification.status, alwaysShowSensitiveData), false); } - public static List notificationListToViewDataList( - List notifications) { - List viewDatas = new ArrayList<>(notifications.size()); - for (Notification n : notifications) { - viewDatas.add(notificationToViewData(n)); - } - return viewDatas; - } - - private static final Function statusMapper = - new Function() { - @Override - public StatusViewData.Concrete apply(Status input) { - return ViewDataUtils.statusToViewData(input); - } - }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java index c90bf5f5c..0beb5632c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java +++ b/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.java @@ -49,6 +49,7 @@ public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration int position = parent.getChildAdapterPosition(child); ThreadAdapter adapter = (ThreadAdapter) parent.getAdapter(); + StatusViewData.Concrete current = adapter.getItem(position); int dividerTop, dividerBottom; if (current != null) { @@ -59,25 +60,17 @@ public class ConversationLineItemDecoration extends RecyclerView.ItemDecoration dividerTop = child.getTop() + avatarMargin; } StatusViewData.Concrete below = adapter.getItem(position + 1); - if (below != null && current.getId().equals(below.getInReplyToId())) { + if (below != null && current.getId().equals(below.getInReplyToId()) && + adapter.getDetailedStatusPosition() != position) { dividerBottom = child.getBottom(); } else { dividerBottom = child.getTop() + avatarMargin; } - } else { - dividerTop = child.getTop(); - if (i == 0) { - dividerTop += avatarMargin; - } - if (i == childCount - 1) { - dividerBottom = child.getTop() + avatarMargin; - } else { - dividerBottom = child.getBottom(); - } - } - divider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom); - divider.draw(c); + divider.setBounds(dividerLeft, dividerTop, dividerRight, dividerBottom); + divider.draw(c); + + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java index d03450174..43a692d97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java @@ -38,13 +38,15 @@ public abstract class NotificationViewData { private final String id; private final Account account; private final StatusViewData.Concrete statusViewData; + private final boolean isExpanded; public Concrete(Notification.Type type, String id, Account account, - StatusViewData.Concrete statusViewData) { + StatusViewData.Concrete statusViewData, boolean isExpanded) { this.type = type; this.id = id; this.account = account; this.statusViewData = statusViewData; + this.isExpanded = isExpanded; } public Notification.Type getType() { @@ -62,6 +64,10 @@ public abstract class NotificationViewData { public StatusViewData.Concrete getStatusViewData() { return statusViewData; } + + public boolean isExpanded() { + return isExpanded; + } } public static final class Placeholder extends NotificationViewData { diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index 2d91ac365..4dfc63901 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.viewdata; import android.support.annotation.Nullable; import android.text.Spanned; +import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Status; @@ -45,14 +46,14 @@ public abstract class StatusViewData { @Nullable private final String spoilerText; private final Status.Visibility visibility; - private final Status.MediaAttachment[] attachments; + private final Attachment[] attachments; @Nullable private final String rebloggedByUsername; @Nullable private final String rebloggedAvatar; private final boolean isSensitive; private final boolean isExpanded; - private final boolean isShowingSensitiveContent; + private final boolean isShowingContent; private final String userFullName; private final String nickname; private final String avatar; @@ -72,9 +73,9 @@ public abstract class StatusViewData { private final Card card; public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, - @Nullable String spoilerText, Status.Visibility visibility, Status.MediaAttachment[] attachments, + @Nullable String spoilerText, Status.Visibility visibility, Attachment[] attachments, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, - boolean isShowingSensitiveWarning, String userFullName, String nickname, String avatar, + boolean isShowingContent, String userFullName, String nickname, String avatar, Date createdAt, String reblogsCount, String favouritesCount, @Nullable String inReplyToId, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, Status.Application application, List emojis, @Nullable Card card) { @@ -89,7 +90,7 @@ public abstract class StatusViewData { this.rebloggedAvatar = rebloggedAvatar; this.isSensitive = sensitive; this.isExpanded = isExpanded; - this.isShowingSensitiveContent = isShowingSensitiveWarning; + this.isShowingContent = isShowingContent; this.userFullName = userFullName; this.nickname = nickname; this.avatar = avatar; @@ -130,7 +131,7 @@ public abstract class StatusViewData { return visibility; } - public Status.MediaAttachment[] getAttachments() { + public Attachment[] getAttachments() { return attachments; } @@ -147,8 +148,8 @@ public abstract class StatusViewData { return isExpanded; } - public boolean isShowingSensitiveContent() { - return isShowingSensitiveContent; + public boolean isShowingContent() { + return isShowingContent; } @Nullable @@ -232,12 +233,12 @@ public abstract class StatusViewData { private boolean favourited; private String spoilerText; private Status.Visibility visibility; - private Status.MediaAttachment[] attachments; + private Attachment[] attachments; private String rebloggedByUsername; private String rebloggedAvatar; private boolean isSensitive; private boolean isExpanded; - private boolean isShowingSensitiveContent; + private boolean isShowingContent; private String userFullName; private String nickname; private String avatar; @@ -267,7 +268,7 @@ public abstract class StatusViewData { rebloggedAvatar = viewData.rebloggedAvatar; isSensitive = viewData.isSensitive; isExpanded = viewData.isExpanded; - isShowingSensitiveContent = viewData.isShowingSensitiveContent; + isShowingContent = viewData.isShowingContent; userFullName = viewData.userFullName; nickname = viewData.nickname; avatar = viewData.avatar; @@ -313,7 +314,7 @@ public abstract class StatusViewData { return this; } - public Builder setAttachments(Status.MediaAttachment[] attachments) { + public Builder setAttachments(Attachment[] attachments) { this.attachments = attachments; return this; } @@ -339,7 +340,7 @@ public abstract class StatusViewData { } public Builder setIsShowingSensitiveContent(boolean isShowingSensitiveContent) { - this.isShowingSensitiveContent = isShowingSensitiveContent; + this.isShowingContent = isShowingSensitiveContent; return this; } @@ -414,7 +415,7 @@ public abstract class StatusViewData { return new StatusViewData.Concrete(id, content, reblogged, favourited, spoilerText, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, - isShowingSensitiveContent, userFullName, nickname, avatar, createdAt, reblogsCount, + isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, emojis, card); } diff --git a/app/src/main/res/drawable/conversation_divider_dark.xml b/app/src/main/res/drawable/conversation_divider_dark.xml deleted file mode 100644 index 42034487b..000000000 --- a/app/src/main/res/drawable/conversation_divider_dark.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_thread_line_dark.xml b/app/src/main/res/drawable/conversation_thread_line_dark.xml new file mode 100644 index 000000000..f19dedfb1 --- /dev/null +++ b/app/src/main/res/drawable/conversation_thread_line_dark.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_thread_line_light.xml b/app/src/main/res/drawable/conversation_thread_line_light.xml new file mode 100644 index 000000000..1d01884ed --- /dev/null +++ b/app/src/main/res/drawable/conversation_thread_line_light.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/favourite_inactive_dark.xml b/app/src/main/res/drawable/favourite_inactive_dark.xml index 24b69ac3c..803131bc1 100644 --- a/app/src/main/res/drawable/favourite_inactive_dark.xml +++ b/app/src/main/res/drawable/favourite_inactive_dark.xml @@ -1,9 +1,9 @@ - +android:width="24dp" +android:height="24dp" +android:viewportWidth="24.0" +android:viewportHeight="24.0"> + diff --git a/app/src/main/res/drawable/favourite_inactive_light.xml b/app/src/main/res/drawable/favourite_inactive_light.xml index 62f63cac6..06ccf499e 100644 --- a/app/src/main/res/drawable/favourite_inactive_light.xml +++ b/app/src/main/res/drawable/favourite_inactive_light.xml @@ -1,9 +1,10 @@ - +android:width="24dp" +android:height="24dp" +android:viewportWidth="24.0" +android:viewportHeight="24.0"> + + diff --git a/app/src/main/res/drawable/ic_person_add_24dp.xml b/app/src/main/res/drawable/ic_person_add_24dp.xml index 5c029e12a..2f4b9dd0c 100644 --- a/app/src/main/res/drawable/ic_person_add_24dp.xml +++ b/app/src/main/res/drawable/ic_person_add_24dp.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_play_48dp.xml b/app/src/main/res/drawable/ic_play_48dp.xml deleted file mode 100644 index a0275bfa5..000000000 --- a/app/src/main/res/drawable/ic_play_48dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_play_indicator_dark.xml b/app/src/main/res/drawable/ic_play_indicator_dark.xml new file mode 100644 index 000000000..62897c4d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_indicator_dark.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/ic_play_indicator_light.xml b/app/src/main/res/drawable/ic_play_indicator_light.xml new file mode 100644 index 000000000..b4c55df51 --- /dev/null +++ b/app/src/main/res/drawable/ic_play_indicator_light.xml @@ -0,0 +1,15 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reblog_dark_18dp.xml b/app/src/main/res/drawable/ic_reblog_dark_18dp.xml new file mode 100644 index 000000000..1a82a0f14 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_dark_18dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_reblog_light_18dp.xml b/app/src/main/res/drawable/ic_reblog_light_18dp.xml new file mode 100644 index 000000000..03904a03d --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_light_18dp.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_reply_24dp.xml b/app/src/main/res/drawable/ic_reply_24dp.xml index c9602ca5a..31b63540b 100644 --- a/app/src/main/res/drawable/ic_reply_24dp.xml +++ b/app/src/main/res/drawable/ic_reply_24dp.xml @@ -1,10 +1,10 @@ + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" /> diff --git a/app/src/main/res/drawable/ic_reply_all_24dp.xml b/app/src/main/res/drawable/ic_reply_all_24dp.xml new file mode 100644 index 000000000..8d4c8d3c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_all_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 16359a8dd..1e111e4fb 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -132,36 +132,36 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="8dp" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/account_note" - tools:text="3000 Followers" android:background="@android:color/transparent" android:textColor="@color/account_tab_font_color" - app:layout_constraintHorizontal_bias="0"/> + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/account_note" + tools:text="3000 Followers" /> + tools:text="500 Following" /> + app:layout_constraintStart_toEndOf="@id/following_tv" + app:layout_constraintTop_toTopOf="@id/followers_tv" + tools:text="3000 Posts" /> @@ -192,7 +192,9 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="?android:colorBackground" - app:tabSelectedTextColor="?attr/colorAccent" > + app:tabGravity="fill" + app:tabMaxWidth="0dp" + app:tabSelectedTextColor="?attr/colorAccent"> + android:inputType="text|textMultiLine|textCapSentences" + android:textSize="20sp" /> - - + android:paddingLeft="14dp" + android:paddingRight="14dp"> - + android:drawableLeft="@drawable/ic_person_add_24dp" + android:drawablePadding="10dp" + android:drawableStart="@drawable/ic_person_add_24dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:maxLines="1" + android:paddingLeft="28dp" + android:paddingStart="28dp" + android:textColor="?android:textColorTertiary" + tools:text="Someone followed you" /> - + - + - + - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 2a3182c27..68c940d59 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -5,48 +5,31 @@ android:id="@+id/status_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" - android:paddingRight="16dp"> + android:paddingLeft="14dp" + android:paddingRight="14dp"> - - - - - - - + android:paddingLeft="38dp" + android:paddingStart="38dp" + android:textColor="?android:textColorTertiary" + tools:text="ConnyDuck boosted" /> @@ -67,54 +50,51 @@ android:id="@+id/status_name_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@+id/status_reblogged_bar" + android:layout_below="@+id/status_reblogged" android:layout_toEndOf="@+id/status_avatar" android:layout_toRightOf="@+id/status_avatar" android:paddingBottom="4dp" android:paddingTop="@dimen/status_avatar_padding"> + + + + - - - - - - - - + android:textColor="?android:textColorSecondary" + tools:text="13:37" /> @@ -134,6 +114,7 @@ android:id="@+id/status_content_warning_description" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorPrimary" /> + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + tools:text="This is a status" /> - - + + + + + + + + + + + + + + + + + + android:alpha="0.7" + android:contentDescription="@null" + android:padding="@dimen/status_sensitive_media_button_padding" + android:visibility="gone" + app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_container" + app:srcCompat="@drawable/ic_remove_red_eye_black_24dp" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:textAlignment="center" + android:textColor="@android:color/white" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + + android:paddingBottom="4dp"> @@ -351,14 +344,12 @@ @@ -371,17 +362,14 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index 9e5ddcf95..f76b854d2 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -5,16 +5,16 @@ android:id="@+id/status_container" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" - android:paddingRight="16dp"> + android:paddingLeft="14dp" + android:paddingRight="14dp"> @@ -22,11 +22,12 @@ - - + android:textStyle="normal|bold" + tools:text="Display Name" /> + android:textColor="?android:textColorSecondary" + tools:text="\@ConnyDuck\@mastodon.social" /> @@ -60,8 +58,6 @@ android:layout_height="wrap_content" android:layout_below="@+id/status_name_bar" android:layout_marginBottom="4dp" - android:layout_toEndOf="@+id/status_avatar" - android:layout_toRightOf="@+id/status_avatar" android:focusable="true" android:visibility="gone" app:paddingHorizontal="4dp"> @@ -70,7 +66,7 @@ android:id="@+id/status_content_warning_description" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="@android:style/TextAppearance.DeviceDefault.Medium" + android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorPrimary" /> @@ -161,184 +151,171 @@ android:textColor="?android:textColorTertiary" /> - - - + android:layout_marginBottom="4dp" + android:layout_marginTop="@dimen/status_media_preview_top_margin"> - + + + + + + + + + + + + + + + + + android:alpha="0.7" + android:contentDescription="@string/action_hide_media" + android:padding="@dimen/status_sensitive_media_button_padding" + android:visibility="gone" + app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_container" + app:srcCompat="@drawable/ic_remove_red_eye_black_24dp" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:textAlignment="center" + android:textColor="@android:color/white" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - - - - - - - + android:layout_marginBottom="6dp" + android:layout_marginTop="10dp" + android:textColor="?android:textColorTertiary" /> + android:paddingBottom="4dp" + android:paddingTop="4dp"> - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_status_notification.xml b/app/src/main/res/layout/item_status_notification.xml index f8e12c02e..719bc796c 100644 --- a/app/src/main/res/layout/item_status_notification.xml +++ b/app/src/main/res/layout/item_status_notification.xml @@ -6,37 +6,74 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" - android:paddingLeft="16dp" - android:paddingRight="16dp"> + android:paddingLeft="14dp" + android:paddingRight="14dp"> + + - - + android:layout_below="@+id/notification_top_text" + android:layout_toEndOf="@+id/notification_status_avatar" + android:layout_toRightOf="@+id/notification_status_avatar" + android:paddingBottom="4dp" + android:paddingTop="6dp"> + android:paddingEnd="@dimen/status_display_name_right_padding" + android:paddingLeft="0dp" + android:paddingRight="@dimen/status_display_name_right_padding" + android:paddingStart="0dp" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.Small" + android:textColor="?android:textColorTertiary" + android:textStyle="normal|bold" + tools:text="Ente" /> + + + + @@ -44,7 +81,7 @@ android:id="@+id/notification_content_warning_bar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_below="@+id/notification_top_bar" + android:layout_below="@+id/status_name_bar" android:layout_marginBottom="4dp" android:layout_toEndOf="@+id/notification_status_avatar" android:layout_toRightOf="@+id/notification_status_avatar" @@ -57,8 +94,9 @@ android:id="@+id/notification_content_warning_description" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" android:textColor="?android:textColorTertiary" - tools:text="Example CW text"/> + tools:text="Example CW text" /> @@ -96,11 +133,11 @@ android:id="@+id/notification_status_avatar" android:layout_width="48dp" android:layout_height="48dp" - android:layout_below="@id/notification_top_bar" - android:layout_marginBottom="8dp" - android:layout_marginEnd="10dp" - android:layout_marginRight="10dp" - android:layout_marginTop="11dp" + android:layout_below="@id/notification_top_text" + android:layout_marginBottom="14dp" + android:layout_marginEnd="14dp" + android:layout_marginRight="14dp" + android:layout_marginTop="10dp" android:contentDescription="@string/action_view_profile" android:paddingBottom="12dp" android:paddingRight="12dp" @@ -114,9 +151,6 @@ android:layout_height="24dp" android:layout_alignBottom="@+id/notification_status_avatar" android:layout_alignEnd="@id/notification_status_avatar" - android:layout_alignRight="@id/notification_status_avatar" - android:visibility="gone" - tools:src="@color/accent" - tools:visibility="visible" /> + android:layout_alignRight="@id/notification_status_avatar" /> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b8f738125..d4dd2a8fc 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -33,8 +33,8 @@ \@%s %s teilte - Sensible Medien - Tippe um zu zeigen + Heikle Inhalte + Zum Anzeigen tippen Zeige mehr Zeige weniger diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 87f89247e..7b40584a5 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -36,8 +36,7 @@ \@%s %s podbił - Wrażliwe treści - Dotknij, aby wyświetlić + Wrażliwe treści\nDotknij, aby wyświetlić Pokaż więcej Ukryj diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 690ad7948..a5184b559 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -14,6 +14,7 @@ + @@ -23,6 +24,7 @@ + @@ -43,4 +45,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f2cb9700b..a4bc41cb1 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,11 +1,12 @@ 4dp 4dp - 8dp + 10dp 8dp 40dp 4dp - 96dp + 100dp + 130dp 8dp 8dp 0dp @@ -16,9 +17,9 @@ 64dp 40dp 8dp - 8dp + 14dp 8dp - 38dp + 36dp 16dp 5dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index 191d5a102..4419acfcf 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -7,6 +7,8 @@ com.keylesspalace.tusky com.keylesspalace.tusky.PREFERENCES + <b>%1$s</b><br>%2$s + 15 20 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 030180e21..bde69c8b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,7 +39,8 @@ \@%s %s boosted - Sensitive Media + Sensitive content + Media hidden Click to view Show More Show Less diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index c2f3ce63f..829d8778a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -19,6 +19,7 @@