[quote] Implement quote feature

This commit is contained in:
kyori19 2019-09-03 23:08:13 +09:00
parent 27a9fc1438
commit 573be935a7
42 changed files with 707 additions and 66 deletions

View File

@ -349,7 +349,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
accountDisplayNameTextView.text = CustomEmojiHelper.emojifyString(account.name, account.emojis, accountDisplayNameTextView)
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this)
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this, false)
accountFieldAdapter.fields = account.fields ?: emptyList()
accountFieldAdapter.emojis = account.emojis ?: emptyList()

View File

@ -17,10 +17,10 @@ package com.keylesspalace.tusky
import android.content.Intent
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import com.google.android.material.bottomsheet.BottomSheetBehavior
import android.view.View
import android.widget.LinearLayout
import androidx.annotation.VisibleForTesting
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.entity.SearchResults
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.LinkHelper
@ -173,6 +173,7 @@ abstract class BottomSheetActivity : BaseActivity() {
// https://pleroma.foo.bar/users/43456787654678
// https://pleroma.foo.bar/notice/43456787654678
// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
// https://mastodon.foo.bar/users/User/statuses/000000000000000000
fun looksLikeMastodonUrl(urlString: String): Boolean {
val uri: URI
try {
@ -192,5 +193,6 @@ fun looksLikeMastodonUrl(urlString: String): Boolean {
path.matches("^/users/[^/]+$".toRegex()) ||
path.matches("^/@[^/]+/\\d+$".toRegex()) ||
path.matches("^/notice/\\d+$".toRegex()) ||
path.matches("^/objects/[-a-f0-9]+$".toRegex())
path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
path.matches("^/users/[^/]+/statuses/[0-9]+$".toRegex())
}

View File

@ -187,6 +187,8 @@ public final class ComposeActivity
private static final String SAVED_JSON_DESCRIPTIONS_EXTRA = "saved_json_descriptions";
private static final String TOOT_VISIBILITY_EXTRA = "toot_visibility";
private static final String IN_REPLY_TO_ID_EXTRA = "in_reply_to_id";
private static final String QUOTE_ID_EXTRA = "quote_id";
private static final String QUOTE_URL_EXTRA = "quote_url";
private static final String REPLY_VISIBILITY_EXTRA = "reply_visibility";
private static final String CONTENT_WARNING_EXTRA = "content_warning";
private static final String MENTIONED_USERNAMES_EXTRA = "mentioned_usernames";
@ -201,6 +203,7 @@ public final class ComposeActivity
private static final int MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420;
private static final String[] CAN_USE_UNLEAKABLE = {"itabashi.0j0.jp", "odakyu.app"};
private static final String[] CAN_USE_QUOTE_ID = {"odakyu.app", "biwakodon.com", "dtp-mstdn.jp", "nitiasa.com"};
@Inject
public MastodonApi mastodonApi;
@ -235,6 +238,8 @@ public final class ComposeActivity
// this only exists when a status is trying to be sent, but uploads are still occurring
private ProgressDialog finishingUploadDialog;
private String inReplyToId;
private String quoteId;
private String quoteUrl;
private List<QueuedMedia> mediaQueued = new ArrayList<>();
private CountUpDownLatch waitForMediaLatch;
private NewPoll poll;
@ -270,6 +275,7 @@ public final class ComposeActivity
replyTextView = findViewById(R.id.composeReplyView);
replyContentTextView = findViewById(R.id.composeReplyContentView);
TextView quoteTextView = findViewById(R.id.composeQuoteView);
textEditor = findViewById(R.id.composeEditField);
mediaPreviewBar = findViewById(R.id.compose_media_preview_bar);
contentWarningBar = findViewById(R.id.composeContentWarningBar);
@ -441,6 +447,8 @@ public final class ComposeActivity
ArrayList<String> loadedDraftMediaDescriptions = null;
ArrayList<Attachment> mediaAttachments = null;
inReplyToId = null;
quoteId = null;
quoteUrl = null;
if (intent != null) {
if (startingVisibility == Status.Visibility.UNKNOWN) {
@ -453,6 +461,14 @@ public final class ComposeActivity
inReplyToId = intent.getStringExtra(IN_REPLY_TO_ID_EXTRA);
quoteId = intent.getStringExtra(QUOTE_ID_EXTRA);
if (intent.hasExtra(QUOTE_URL_EXTRA)) {
quoteTextView.setVisibility(View.VISIBLE);
quoteUrl = intent.getStringExtra(QUOTE_URL_EXTRA);
quoteTextView.setText(String.format(getString(R.string.quote_to), quoteUrl));
}
mentionedUsernames = intent.getStringArrayExtra(MENTIONED_USERNAMES_EXTRA);
String contentWarning = intent.getStringExtra(CONTENT_WARNING_EXTRA);
@ -1086,7 +1102,7 @@ public final class ComposeActivity
}
private void sendStatus(String content, Status.Visibility visibility, boolean sensitive,
String spoilerText) {
String spoilerText, @Nullable String quoteId, @Nullable String quoteUrl) {
ArrayList<String> mediaIds = new ArrayList<>();
ArrayList<Uri> mediaUris = new ArrayList<>();
ArrayList<String> mediaDescriptions = new ArrayList<>();
@ -1096,12 +1112,22 @@ public final class ComposeActivity
mediaDescriptions.add(item.description);
}
Intent sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
Intent sendIntent;
AccountEntity activeAccount = accountManager.getActiveAccount();
if (activeAccount != null
&& !Arrays.asList(CAN_USE_QUOTE_ID).contains(activeAccount.getDomain())
&& quoteUrl != null) {
content += "\n~~~~~~~~~~\n[" + quoteUrl + "]";
quoteId = null;
}
sendIntent = SendTootService.sendTootIntent(this, content, spoilerText,
visibility, !mediaUris.isEmpty() && sensitive, mediaIds, mediaUris, mediaDescriptions, inReplyToId, poll,
getIntent().getStringExtra(REPLYING_STATUS_CONTENT_EXTRA),
getIntent().getStringExtra(REPLYING_STATUS_AUTHOR_USERNAME_EXTRA),
getIntent().getStringExtra(SAVED_JSON_URLS_EXTRA),
accountManager.getActiveAccount(), savedTootUid);
quoteId, accountManager.getActiveAccount(), savedTootUid);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(sendIntent);
@ -1169,7 +1195,7 @@ public final class ComposeActivity
textEditor.setError(getString(R.string.error_empty));
enableButtons();
} else if (characterCount <= maximumTootCharacters) {
sendStatus(contentText, visibility, sensitive, spoilerText);
sendStatus(contentText, visibility, sensitive, spoilerText, quoteId, quoteUrl);
} else {
textEditor.setError(getString(R.string.error_compose_character_limit));
@ -2074,6 +2100,10 @@ public final class ComposeActivity
@Nullable
private String inReplyToId;
@Nullable
private String quoteId;
@Nullable
private String quoteUrl;
@Nullable
private Status.Visibility replyVisibility;
@Nullable
private Status.Visibility visibility;
@ -2125,6 +2155,16 @@ public final class ComposeActivity
return this;
}
public IntentBuilder quoteId(String quoteId) {
this.quoteId = quoteId;
return this;
}
public IntentBuilder quoteUrl(String quoteUrl) {
this.quoteUrl = quoteUrl;
return this;
}
public IntentBuilder replyVisibility(Status.Visibility replyVisibility) {
this.replyVisibility = replyVisibility;
return this;
@ -2182,6 +2222,12 @@ public final class ComposeActivity
if (inReplyToId != null) {
intent.putExtra(IN_REPLY_TO_ID_EXTRA, inReplyToId);
}
if (quoteId != null) {
intent.putExtra(QUOTE_ID_EXTRA, quoteId);
}
if (quoteUrl != null) {
intent.putExtra(QUOTE_URL_EXTRA, quoteUrl);
}
if (replyVisibility != null) {
intent.putExtra(REPLY_VISIBILITY_EXTRA, replyVisibility.getNum());
}

View File

@ -15,11 +15,11 @@
package com.keylesspalace.tusky.adapter
import androidx.recyclerview.widget.RecyclerView
import android.view.LayoutInflater
import android.view.ViewGroup
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Field
@ -47,7 +47,7 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
viewHolder.nameTextView.text = emojifiedName
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener)
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener, false)
if(field.verifiedAt != null) {
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)

View File

@ -33,10 +33,18 @@ import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.recyclerview.widget.RecyclerView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
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;
@ -48,17 +56,13 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils;
import net.accelf.yuito.QuoteInlineHelper;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.text.BidiFormatter;
import androidx.recyclerview.widget.RecyclerView;
public class NotificationsAdapter extends RecyclerView.Adapter {
public interface AdapterDataSource<T> {
@ -352,6 +356,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private final TextView contentWarningDescriptionTextView;
private final ToggleButton contentWarningButton;
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
private ConstraintLayout quoteContainer;
private String accountId;
private String notificationId;
@ -377,6 +382,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
quoteContainer = itemView.findViewById(R.id.status_quote_inline_container);
int darkerFilter = Color.rgb(123, 123, 123);
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
notificationAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
@ -520,6 +527,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
notificationAvatar, notificationAvatarRadius, animateAvatar);
}
private void setQuoteContainer(Status status, final LinkListener listener) {
if (status != null) {
quoteContainer.setVisibility(View.VISIBLE);
new QuoteInlineHelper(status, quoteContainer, listener).setupQuoteContainer();
} else {
quoteContainer.setVisibility(View.GONE);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
@ -570,11 +586,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent);
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener,
notificationViewData.getStatusViewData().getQuote() != null);
Spanned emojifiedContentWarning =
CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView);
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
setQuoteContainer(statusViewData.getQuote(), listener);
}
@Override

View File

@ -1,6 +1,8 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.text.Spanned;
import android.text.TextUtils;
@ -16,6 +18,7 @@ import android.widget.ToggleButton;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@ -41,6 +44,8 @@ import com.keylesspalace.tusky.viewdata.PollViewDataKt;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import com.mikepenz.iconics.utils.Utils;
import net.accelf.yuito.QuoteInlineHelper;
import java.text.NumberFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -62,6 +67,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private ImageButton replyButton;
private SparkButton reblogButton;
private SparkButton favouriteButton;
private ImageButton quoteButton;
private ImageButton moreButton;
protected MediaPreviewImageView[] mediaPreviews;
private ImageView[] mediaOverlays;
@ -76,6 +82,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public TextView content;
public TextView contentWarningDescription;
private ConstraintLayout quoteContainer;
private RecyclerView pollOptions;
private TextView pollDescription;
private Button pollButton;
@ -105,6 +113,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
replyButton = itemView.findViewById(R.id.status_reply);
reblogButton = itemView.findViewById(R.id.status_inset);
favouriteButton = itemView.findViewById(R.id.status_favourite);
quoteButton = itemView.findViewById(R.id.status_quote);
moreButton = itemView.findViewById(R.id.status_more);
mediaPreviews = new MediaPreviewImageView[]{
@ -131,6 +140,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
avatarInset = itemView.findViewById(R.id.status_avatar_inset);
quoteContainer = itemView.findViewById(R.id.status_quote_inline_container);
pollOptions = itemView.findViewById(R.id.status_poll_options);
pollDescription = itemView.findViewById(R.id.status_poll_description);
pollButton = itemView.findViewById(R.id.status_poll_button);
@ -174,11 +185,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
@Nullable String spoilerText,
@Nullable Status.Mention[] mentions,
@NonNull List<Emoji> emojis,
final StatusActionListener listener) {
final StatusActionListener listener,
boolean removeQuote) {
if (TextUtils.isEmpty(spoilerText)) {
contentWarningDescription.setVisibility(View.GONE);
contentWarningButton.setVisibility(View.GONE);
this.setTextVisible(true, content, mentions, emojis, listener);
this.setTextVisible(true, content, mentions, emojis, listener, removeQuote);
} else {
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
contentWarningDescription.setText(emojiSpoiler);
@ -189,9 +201,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
listener.onExpandedChange(isChecked, getAdapterPosition());
}
this.setTextVisible(isChecked, content, mentions, emojis, listener);
this.setTextVisible(isChecked, content, mentions, emojis, listener, removeQuote);
});
this.setTextVisible(expanded, content, mentions, emojis, listener);
this.setTextVisible(expanded, content, mentions, emojis, listener, removeQuote);
}
}
@ -199,10 +211,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Spanned content,
Status.Mention[] mentions,
List<Emoji> emojis,
final StatusActionListener listener) {
final StatusActionListener listener,
boolean removeQuote) {
if (expanded) {
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener, removeQuote);
} else {
LinkHelper.setClickableMentions(this.content, mentions, listener);
}
@ -334,10 +347,37 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
}
private void setQuoteEnabled(boolean enabled, Status.Visibility visibility) {
quoteButton.setEnabled(enabled && visibility != Status.Visibility.PRIVATE && visibility != Status.Visibility.UNLEAKABLE);
if (enabled && visibility != Status.Visibility.PRIVATE && visibility != Status.Visibility.UNLEAKABLE) {
int activeId;
activeId = ThemeUtils.getDrawableId(quoteButton.getContext(),
R.attr.status_quote_drawable, R.drawable.ic_quote_24dp);
quoteButton.setImageResource(activeId);
} else {
Resources res = quoteButton.getContext().getResources();
Drawable disableIcon = res.getDrawable(R.drawable.ic_quote_disabled_24dp);
if (disableIcon != null) {
disableIcon.setColorFilter(res.getColor(R.color.status_reblog_button_disabled_dark), PorterDuff.Mode.DST_IN);
}
quoteButton.setImageDrawable(disableIcon);
}
}
protected void setFavourited(boolean favourited) {
favouriteButton.setChecked(favourited);
}
private void setQuoteContainer(Status status, final StatusActionListener listener) {
if (status != null) {
quoteContainer.setVisibility(View.VISIBLE);
new QuoteInlineHelper(status, quoteContainer, listener).setupQuoteContainer();
} else {
quoteContainer.setVisibility(View.GONE);
}
}
private void loadImage(MediaPreviewImageView imageView, String previewUrl, String description,
MetaData meta) {
if (TextUtils.isEmpty(previewUrl)) {
@ -568,6 +608,16 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public void onEventAnimationStart(ImageView button, boolean buttonState) {
}
});
if (quoteButton != null) {
quoteButton.setOnClickListener(view -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onQuote(position);
}
});
}
moreButton.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
@ -607,6 +657,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setAvatar(status.getAvatar(), status.getRebloggedAvatar(), status.isBot(), showBotOverlay, animateAvatar);
setReblogged(status.isReblogged());
setFavourited(status.isFavourited());
setQuoteContainer(status.getQuote(), listener);
List<Attachment> attachments = status.getAttachments();
boolean sensitive = status.isSensitive();
if (mediaPreviewEnabled) {
@ -631,8 +682,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
setupButtons(listener, status.getSenderId());
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
setQuoteEnabled(status.getRebloggingEnabled(), status.getVisibility());
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener,
status.getQuote() != null);
setDescriptionForStatus(status);

View File

@ -3,6 +3,7 @@ package com.keylesspalace.tusky.adapter;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
@ -16,9 +17,14 @@ import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.ViewThreadActivity;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
@ -28,10 +34,8 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
import java.util.Date;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.wasabeef.glide.transformations.RoundedCornersTransformation;
@ -209,7 +213,22 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
cardView.setOnClickListener(v -> LinkHelper.openLink(card.getUrl(), v.getContext()));
cardView.setOnClickListener(v -> {
String url = card.getUrl();
String regex = ".*/users/[^/]+/statuses/([0-9]+)";
String replace = "$1";
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(url);
if (m.find()) {
String id = m.replaceAll(replace);
Intent intent = new Intent(v.getContext(), ViewThreadActivity.class);
intent.putExtra("id", id);
intent.putExtra("url", url);
v.getContext().startActivity(intent);
} else {
LinkHelper.openLink(url, v.getContext());
}
});
} else {
cardView.setVisibility(View.GONE);

View File

@ -156,7 +156,8 @@ data class ConversationStatusEntity(
application = null,
pinned = false,
poll = poll,
card = null)
card = null,
quote = null)
}
}

View File

@ -107,7 +107,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setupButtons(listener, account.getId());
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), listener);
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), listener, false);
setConversationName(conversation.getAccounts());

View File

@ -117,6 +117,10 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
viewModel.favourite(favourite, position)
}
override fun onQuote(position: Int) {
// its impossible to quote private messages
}
override fun onMore(view: View, position: Int) {
viewModel.conversations.value?.getOrNull(position)?.lastStatus?.let {
more(it.toStatus(), view, position)

View File

@ -83,7 +83,8 @@ class StatusViewHolder(itemView: View,
viewState.isContentShow(status.id, status.sensitive), status.spoilerText)
if (status.spoilerText.isBlank()) {
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler)
setTextVisible(true, status.content, status.mentions, status.emojis, adapterHandler,
status.quote != null)
itemView.statusContentWarningButton.hide()
itemView.statusContentWarningDescription.hide()
} else {
@ -96,10 +97,12 @@ class StatusViewHolder(itemView: View,
status()?.let { status ->
itemView.statusContentWarningDescription.invalidate()
viewState.setContentShow(status.id, isViewChecked)
setTextVisible(isViewChecked, status.content, status.mentions, status.emojis, adapterHandler)
setTextVisible(isViewChecked, status.content, status.mentions, status.emojis, adapterHandler,
status.quote != null)
}
}
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler)
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.emojis, adapterHandler,
status.quote != null)
}
}
}
@ -109,10 +112,11 @@ class StatusViewHolder(itemView: View,
content: Spanned,
mentions: Array<Status.Mention>?,
emojis: List<Emoji>,
listener: LinkListener) {
listener: LinkListener,
removeQuote: Boolean) {
if (expanded) {
val emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, itemView.statusContent)
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener)
LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener, removeQuote)
} else {
LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener)
}

View File

@ -94,6 +94,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
}
}
override fun onQuote(position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let { status ->
quote(status)
}
}
override fun onMore(view: View, position: Int) {
(adapter as? SearchStatusesAdapter)?.getItem(position)?.first?.let {
more(it, view, position)
@ -200,6 +206,28 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
requireActivity().startActivity(intent)
}
private fun quote(status: Status) {
val id = status.actionableId
val actionableStatus = status.actionableStatus
val visibility = actionableStatus.visibility
val url = actionableStatus.url
val mentions = actionableStatus.mentions
val mentionedUsernames = LinkedHashSet<String>()
mentionedUsernames.add(actionableStatus.account.username)
val loggedInUsername = viewModel.activeAccount?.username
for ((_, _, username) in mentions) {
mentionedUsernames.add(username)
}
mentionedUsernames.remove(loggedInUsername)
val intent = ComposeActivity.IntentBuilder()
.quoteId(id)
.quoteUrl(url)
.replyVisibility(visibility)
.mentionedUsernames(mentionedUsernames)
.build(context)
startActivity(intent)
}
private fun more(status: Status, view: View, position: Int) {
val id = status.actionableId
val accountId = status.actionableStatus.account.id

View File

@ -26,7 +26,8 @@ data class NewStatus(
val visibility: String,
val sensitive: Boolean,
@SerializedName("media_ids") val mediaIds: List<String>?,
val poll: NewPoll?
val poll: NewPoll?,
@SerializedName("quote_id") val quoteId: String?
)
@Parcelize

View File

@ -43,7 +43,8 @@ data class Status(
val application: Application?,
var pinned: Boolean?,
val poll: Poll?,
val card: Card?
val card: Card?,
val quote: Status?
) {
val actionableId: String

View File

@ -462,6 +462,11 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
}
@Override
public void onQuote(int position) {
super.quote(notifications.get(position).asRight().getStatus());
}
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
final Notification notification = notifications.get(position).asRight();
final Status status = notification.getStatus();

View File

@ -163,6 +163,35 @@ public abstract class SFragment extends BaseFragment implements Injectable {
getActivity().startActivity(intent);
}
protected void quote(Status status) {
String id = status.getActionableId();
Status actionableStatus = status.getActionableStatus();
Status.Visibility visibility = actionableStatus.getVisibility();
String url = actionableStatus.getUrl();
Status.Mention[] mentions = actionableStatus.getMentions();
Set<String> mentionedUsernames = new LinkedHashSet<>();
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
String loggedInUsername = null;
AccountEntity activeAccount = accountManager.getActiveAccount();
if(activeAccount != null) {
loggedInUsername = activeAccount.getUsername();
}
for (Status.Mention mention : mentions) {
mentionedUsernames.add(mention.getUsername());
}
mentionedUsernames.remove(loggedInUsername);
if (status.getReblog() != null) {
url = status.getReblog().getUrl();
}
Intent intent = new ComposeActivity.IntentBuilder()
.quoteId(id)
.quoteUrl(url)
.replyVisibility(visibility)
.mentionedUsernames(mentionedUsernames)
.build(getContext());
startActivity(intent);
}
protected void more(@NonNull final Status status, View view, final int position) {
final String id = status.getActionableId();
final String accountId = status.getActionableStatus().getAccount().getId();

View File

@ -631,6 +631,11 @@ public class TimelineFragment extends SFragment implements
updateAdapter();
}
@Override
public void onQuote(int position) {
super.quote(statuses.get(position).asRight());
}
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
final Status status = statuses.get(position).asRight();

View File

@ -257,6 +257,11 @@ public final class ViewThreadFragment extends SFragment implements
);
}
@Override
public void onQuote(int position) {
super.quote(statuses.get(position));
}
private void updateStatus(int position, Status status) {
if (position >= 0 && position < statuses.size()) {

View File

@ -17,15 +17,16 @@ package com.keylesspalace.tusky.interfaces;
import android.view.View;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
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 onQuote(int position);
void onMore(@NonNull View view, final int position);
void onViewMedia(int position, int attachmentIndex, @Nullable View view);
void onViewThread(int position);

View File

@ -15,6 +15,8 @@
package com.keylesspalace.tusky.network;
import androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.AccessToken;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.AppCredentials;
@ -37,7 +39,6 @@ import com.keylesspalace.tusky.entity.StatusContext;
import java.util.List;
import java.util.Set;
import androidx.annotation.Nullable;
import io.reactivex.Completable;
import io.reactivex.Single;
import okhttp3.MultipartBody;

View File

@ -18,11 +18,11 @@ package com.keylesspalace.tusky.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.RemoteInput
import androidx.core.content.ContextCompat
import android.util.Log
import com.keylesspalace.tusky.ComposeActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.db.AccountManager
@ -96,7 +96,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
null,
null,
null,
null, account, 0)
null, null, account, 0)
context.startService(sendIntent)

View File

@ -229,7 +229,8 @@ class TimelineRepositoryImpl(
application = application,
pinned = false,
poll = poll,
card = null
card = null,
quote = null
)
}
val status = if (reblog != null) {
@ -255,7 +256,8 @@ class TimelineRepositoryImpl(
application = null,
pinned = false,
poll = null,
card = null
card = null,
quote = null
)
} else {
Status(
@ -280,7 +282,8 @@ class TimelineRepositoryImpl(
application = application,
pinned = false,
poll = poll,
card = null
card = null,
quote = null
)
}
return Either.Right(status)

View File

@ -140,7 +140,8 @@ class SendTootService : Service(), Injectable {
tootToSend.visibility,
tootToSend.sensitive,
tootToSend.mediaIds,
tootToSend.poll
tootToSend.poll,
tootToSend.quoteId
)
val sendCall = mastodonApi.createStatus(
@ -289,6 +290,7 @@ class SendTootService : Service(), Injectable {
replyingStatusContent: String?,
replyingStatusAuthorUsername: String?,
savedJsonUrls: String?,
quoteId: String?,
account: AccountEntity,
savedTootUid: Int
): Intent {
@ -308,6 +310,7 @@ class SendTootService : Service(), Injectable {
replyingStatusContent,
replyingStatusAuthorUsername,
savedJsonUrls,
quoteId,
account.id,
savedTootUid,
idempotencyKey,
@ -351,6 +354,7 @@ data class TootToSend(val text: String,
val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?,
val savedJsonUrls: String?,
val quoteId: String?,
val accountId: Long,
val savedTootUid: Int,
val idempotencyKey: String,

View File

@ -20,10 +20,6 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
@ -33,11 +29,14 @@ import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.browser.customtabs.CustomTabsIntent;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
import java.lang.CharSequence;
import java.net.URI;
import java.net.URISyntaxException;
@ -69,7 +68,8 @@ public class LinkHelper {
* @param listener to notify about particular spans that are clicked
*/
public static void setClickableText(TextView view, Spanned content,
@Nullable Status.Mention[] mentions, final LinkListener listener) {
@Nullable Status.Mention[] mentions, final LinkListener listener,
boolean removeQuote) {
SpannableStringBuilder builder = new SpannableStringBuilder(content);
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
for (URLSpan span : urlSpans) {
@ -126,6 +126,13 @@ public class LinkHelper {
builder.subSequence(end, end + 1).toString().equals("\n")){
builder.insert(end, "\u200B");
}
if (start >= 13 && end < builder.length() && removeQuote) {
if (builder.subSequence(start - 13, start).toString().equals("\n~~~~~~~~~~\n[")
&& builder.subSequence(end, end + 1).toString().equals("]")) {
builder.delete(start - 13, end + 1);
}
}
}
view.setText(builder);

View File

@ -64,6 +64,7 @@ public final class ViewDataUtils {
.setPoll(visibleStatus.getPoll())
.setCard(visibleStatus.getCard())
.setIsBot(visibleStatus.getAccount().getBot())
.setQuote(visibleStatus.getQuote())
.createStatusViewData();
}

View File

@ -16,10 +16,11 @@
package com.keylesspalace.tusky.viewdata;
import android.os.Build;
import androidx.annotation.Nullable;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import androidx.annotation.Nullable;
import com.keylesspalace.tusky.entity.Attachment;
import com.keylesspalace.tusky.entity.Card;
import com.keylesspalace.tusky.entity.Emoji;
@ -92,6 +93,8 @@ public abstract class StatusViewData {
private final PollViewData poll;
private final boolean isBot;
private final Status quote;
public Concrete(String id, Spanned content, boolean reblogged, boolean favourited,
@Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
@ -99,7 +102,7 @@ public abstract class StatusViewData {
Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, @Nullable Card card,
boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot) {
boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, Status quote) {
this.id = id;
if (Build.VERSION.SDK_INT == 23) {
@ -138,6 +141,7 @@ public abstract class StatusViewData {
this.isCollapsed = isCollapsed;
this.poll = poll;
this.isBot = isBot;
this.quote = quote;
}
public String getId() {
@ -277,6 +281,10 @@ public abstract class StatusViewData {
return poll;
}
public Status getQuote() {
return quote;
}
@Override public long getViewDataId() {
// Chance of collision is super low and impact of mistake is low as well
return id.hashCode();
@ -313,8 +321,9 @@ public abstract class StatusViewData {
Objects.equals(statusEmojis, concrete.statusEmojis) &&
Objects.equals(accountEmojis, concrete.accountEmojis) &&
Objects.equals(card, concrete.card) &&
Objects.equals(poll, concrete.poll)
&& isCollapsed == concrete.isCollapsed;
Objects.equals(poll, concrete.poll) &&
isCollapsed == concrete.isCollapsed &&
Objects.equals(quote, concrete.quote);
}
static Spanned replaceCrashingCharacters(Spanned content) {
@ -420,6 +429,7 @@ public abstract class StatusViewData {
private boolean isCollapsed; /** Whether the status is shown partially or fully */
private PollViewData poll;
private boolean isBot;
private Status quote;
public Builder() {
}
@ -455,6 +465,7 @@ public abstract class StatusViewData {
isCollapsed = viewData.isCollapsed();
poll = viewData.poll;
isBot = viewData.isBot();
quote = viewData.getQuote();
}
public Builder setId(String id) {
@ -621,6 +632,11 @@ public abstract class StatusViewData {
return this;
}
public Builder setQuote(Status quote){
this.quote = quote;
return this;
}
public StatusViewData.Concrete createStatusViewData() {
if (this.statusEmojis == null) statusEmojis = Collections.emptyList();
if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
@ -630,7 +646,7 @@ public abstract class StatusViewData {
attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot);
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, quote);
}
}
}

View File

@ -0,0 +1,130 @@
package net.accelf.yuito;
import android.content.Context;
import android.text.Spanned;
import android.text.TextUtils;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ToggleButton;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.LinkHelper;
import java.util.List;
public class QuoteInlineHelper {
private Status quoteStatus;
private View quoteContainer;
private ImageView quoteAvatar;
private TextView quoteDisplayName;
private TextView quoteUsername;
private TextView quoteContentWarningDescription;
private ToggleButton quoteContentWarningButton;
private TextView quoteContent;
private TextView quoteMedia;
private LinkListener listener;
public QuoteInlineHelper(Status status, View container, LinkListener listener) {
quoteStatus = status;
quoteContainer = container;
quoteAvatar = container.findViewById(R.id.status_quote_inline_avatar);
quoteDisplayName = container.findViewById(R.id.status_quote_inline_display_name);
quoteUsername = container.findViewById(R.id.status_quote_inline_username);
quoteContentWarningDescription = container.findViewById(R.id.status_quote_inline_content_warning_description);
quoteContentWarningButton = container.findViewById(R.id.status_quote_inline_content_warning_button);
quoteContent = container.findViewById(R.id.status_quote_inline_content);
quoteMedia = container.findViewById(R.id.status_quote_inline_media);
this.listener = listener;
}
private void setDisplayName(String name, List<Emoji> customEmojis) {
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(name, customEmojis, quoteDisplayName);
quoteDisplayName.setText(emojifiedName);
}
private void setUsername(String name) {
Context context = quoteUsername.getContext();
String format = context.getString(R.string.status_username_format);
String usernameText = String.format(format, name);
quoteUsername.setText(usernameText);
}
private void setContent(Spanned content, Status.Mention[] mentions, List<Emoji> emojis,
LinkListener listener) {
Spanned singleLineText = SpannedTextHelper.replaceSpanned(content);
Spanned emojifiedText = CustomEmojiHelper.emojifyText(singleLineText, emojis, quoteContent);
LinkHelper.setClickableText(quoteContent, emojifiedText, mentions, listener, false);
}
private void setAvatar(String url) {
if (TextUtils.isEmpty(url)) {
quoteAvatar.setImageResource(R.drawable.avatar_default);
} else {
Glide.with(quoteAvatar.getContext())
.load(url)
.placeholder(R.drawable.avatar_default)
.into(quoteAvatar);
}
}
private void setSpoilerText(String spoilerText, List<Emoji> emojis) {
CharSequence emojiSpoiler =
CustomEmojiHelper.emojifyString(spoilerText, emojis, quoteContentWarningDescription);
quoteContentWarningDescription.setText(emojiSpoiler);
quoteContentWarningDescription.setVisibility(View.VISIBLE);
quoteContentWarningButton.setVisibility(View.VISIBLE);
quoteContentWarningButton.setChecked(false);
quoteContentWarningButton.setOnCheckedChangeListener((buttonView, isChecked)
-> quoteContent.setVisibility(isChecked ? View.VISIBLE : View.GONE));
quoteContent.setVisibility(View.GONE);
}
private void hideSpoilerText() {
quoteContentWarningDescription.setVisibility(View.GONE);
quoteContentWarningButton.setVisibility(View.GONE);
quoteContent.setVisibility(View.VISIBLE);
}
private void setOnClickListener(String accountId, String statusUrl) {
quoteAvatar.setOnClickListener(view -> listener.onViewAccount(accountId));
quoteDisplayName.setOnClickListener(view -> listener.onViewAccount(accountId));
quoteUsername.setOnClickListener(view -> listener.onViewAccount(accountId));
quoteContent.setOnClickListener(view -> listener.onViewUrl(statusUrl));
quoteMedia.setOnClickListener(view -> listener.onViewUrl(statusUrl));
quoteContainer.setOnClickListener(view -> listener.onViewUrl(statusUrl));
}
public void setupQuoteContainer() {
Account account = quoteStatus.getAccount();
setDisplayName(account.getDisplayName().equals("") ? account.getLocalUsername() : account.getDisplayName(), account.getEmojis());
setUsername(account.getUsername());
setContent(quoteStatus.getContent(), quoteStatus.getMentions(),
quoteStatus.getEmojis(), listener);
setAvatar(account.getAvatar());
setOnClickListener(account.getId(), quoteStatus.getUrl());
if (quoteStatus.getSpoilerText().isEmpty()) {
hideSpoilerText();
} else {
setSpoilerText(quoteStatus.getSpoilerText(), quoteStatus.getEmojis());
}
if (quoteStatus.getAttachments().size() == 0) {
quoteMedia.setVisibility(View.GONE);
} else {
quoteMedia.setVisibility(View.VISIBLE);
quoteMedia.setText(quoteContainer.getContext().getString(R.string.status_quote_media,
quoteStatus.getAttachments().size()));
}
}
}

View File

@ -0,0 +1,22 @@
package net.accelf.yuito;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class SpannedTextHelper {
static Spanned replaceSpanned(Spanned targetText) {
String targetString = targetText.toString();
SpannableStringBuilder builder = new SpannableStringBuilder(targetText);
Pattern pattern = Pattern.compile("\n");
Matcher matcher = pattern.matcher(targetString);
while (matcher.find()) {
builder.replace(matcher.start(), matcher.end(), " ");
}
return builder;
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/toolbar_icon_dark"
android:pathData="m 20.9375,2.375 h -5.5 c -1.138672,0 -2.0625,0.923828 -2.0625,2.0625 v 5.5 c 0,1.138672 0.923828,2.0625 2.0625,2.0625 h 3.4375 v 2.75 c 0,1.516797 -1.233203,2.75 -2.75,2.75 H 15.78125 C 15.209766,17.5 14.75,17.959766 14.75,18.53125 v 2.0625 c 0,0.571484 0.459766,1.03125 1.03125,1.03125 H 16.125 C 19.923437,21.625 23,18.548437 23,14.75 V 4.4375 C 23,3.298828 22.076172,2.375 20.9375,2.375 Z m -12.375,0 h -5.5 C 1.9238281,2.375 1,3.298828 1,4.4375 v 5.5 C 1,11.076172 1.9238281,12 3.0625,12 H 6.5 v 2.75 c 0,1.516797 -1.2332031,2.75 -2.75,2.75 H 3.40625 C 2.8347656,17.5 2.375,17.959766 2.375,18.53125 v 2.0625 c 0,0.571484 0.4597656,1.03125 1.03125,1.03125 H 3.75 c 3.7984375,0 6.875,-3.076563 6.875,-6.875 V 4.4375 C 10.625,3.298828 9.701172,2.375 8.5625,2.375 Z" />
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/status_reblog_button_disabled_dark"
android:pathData="m 20.9375,2.375 h -5.5 c -1.138672,0 -2.0625,0.923828 -2.0625,2.0625 v 5.5 c 0,1.138672 0.923828,2.0625 2.0625,2.0625 h 3.4375 v 2.75 c 0,1.516797 -1.233203,2.75 -2.75,2.75 H 15.78125 C 15.209766,17.5 14.75,17.959766 14.75,18.53125 v 2.0625 c 0,0.571484 0.459766,1.03125 1.03125,1.03125 H 16.125 C 19.923437,21.625 23,18.548437 23,14.75 V 4.4375 C 23,3.298828 22.076172,2.375 20.9375,2.375 Z m -12.375,0 h -5.5 C 1.9238281,2.375 1,3.298828 1,4.4375 v 5.5 C 1,11.076172 1.9238281,12 3.0625,12 H 6.5 v 2.75 c 0,1.516797 -1.2332031,2.75 -2.75,2.75 H 3.40625 C 2.8347656,17.5 2.375,17.959766 2.375,18.53125 v 2.0625 c 0,0.571484 0.4597656,1.03125 1.03125,1.03125 H 3.75 c 3.7984375,0 6.875,-3.076563 6.875,-6.875 V 4.4375 C 10.625,3.298828 9.701172,2.375 8.5625,2.375 Z" />
</vector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<stroke
android:width="2dp"
android:color="#888888" />
</shape>

View File

@ -90,6 +90,18 @@
tools:text="Post content which may be preeettyy long, so please, make sure there's enough room for everything, okay? Not kidding. I wish Eugen answered me more often, sigh."
tools:visibility="visible" />
<TextView
android:id="@+id/composeQuoteView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="6dp"
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:drawablePadding="6dp"
android:textSize="?attr/status_text_small"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:id="@+id/composeContentWarningBar"
android:layout_width="match_parent"

View File

@ -177,6 +177,18 @@
app:layout_constraintTop_toBottomOf="@id/status_content"
tools:visibility="visible" />
<include
android:id="@+id/status_quote_inline_container"
layout="@layout/view_quote_inline"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/button_toggle_content" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/status_media_preview_container"
android:layout_width="0dp"
@ -185,7 +197,7 @@
android:importantForAccessibility="noHideDescendants"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@id/status_display_name"
app:layout_constraintTop_toBottomOf="@id/button_toggle_content"
app:layout_constraintTop_toBottomOf="@id/status_quote_inline_container"
tools:visibility="gone">
<com.keylesspalace.tusky.view.MediaPreviewImageView
@ -449,7 +461,7 @@
android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintEnd_toStartOf="@id/status_quote"
app:layout_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="?attr/status_favourite_active_drawable"
@ -458,6 +470,21 @@
sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" />
<ImageButton
android:id="@+id/status_quote"
style="?attr/image_button_style"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginEnd="4dp"
android:contentDescription="@string/action_quote"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_quote_disabled_24dp" />
<ImageButton
android:id="@+id/status_more"
style="?attr/image_button_style"
@ -469,7 +496,7 @@
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintStart_toEndOf="@id/status_quote"
app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_more_horiz_24dp" />

View File

@ -129,6 +129,18 @@
app:layout_constraintTop_toBottomOf="@+id/status_content_warning_button"
tools:text="Status content. Can be pretty long. " />
<include
android:id="@+id/status_quote_inline_container"
layout="@layout/view_quote_inline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_content" />
<LinearLayout
android:id="@+id/card_view"
android:layout_width="match_parent"
@ -139,7 +151,7 @@
android:foreground="?attr/selectableItemBackground"
android:minHeight="80dp"
android:orientation="vertical"
app:layout_constraintTop_toBottomOf="@+id/status_content"
app:layout_constraintTop_toBottomOf="@+id/status_quote_inline_container"
tools:visibility="gone">
<ImageView
@ -539,7 +551,7 @@
android:contentDescription="@string/action_favourite"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintEnd_toStartOf="@id/status_quote"
app:layout_constraintStart_toEndOf="@id/status_inset"
app:layout_constraintTop_toTopOf="@id/status_inset"
sparkbutton:activeImage="?attr/status_favourite_active_drawable"
@ -548,6 +560,20 @@
sparkbutton:primaryColor="@color/tusky_orange"
sparkbutton:secondaryColor="@color/tusky_orange_light" />
<ImageButton
android:id="@+id/status_quote"
style="?attr/image_button_style"
android:layout_width="40dp"
android:layout_height="40dp"
android:contentDescription="@string/action_quote"
android:importantForAccessibility="no"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toStartOf="@id/status_more"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_quote_disabled_24dp" />
<ImageButton
android:id="@+id/status_more"
style="?attr/image_button_style"
@ -558,7 +584,7 @@
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="@id/status_reply"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/status_favourite"
app:layout_constraintStart_toEndOf="@id/status_quote"
app:layout_constraintTop_toTopOf="@id/status_reply"
app:srcCompat="@drawable/ic_more_horiz_24dp" />

View File

@ -137,6 +137,15 @@
android:textSize="?attr/status_text_medium"
android:visibility="gone" />
<include
android:id="@+id/status_quote_inline_container"
layout="@layout/view_quote_inline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/button_toggle_notification_content"
android:layout_toEndOf="@id/notification_status_avatar"
android:layout_marginBottom="8dp" />
<ImageView
android:id="@+id/notification_status_avatar"
android:layout_width="48dp"

View File

@ -0,0 +1,121 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/quote_inline_frame"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
<ImageView
android:id="@+id/status_quote_inline_avatar"
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/avatar_default" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_quote_inline_display_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="4dp"
android:paddingEnd="4dp"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
android:textStyle="normal|bold"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@id/status_quote_inline_avatar"
app:layout_constraintStart_toEndOf="@id/status_quote_inline_avatar"
app:layout_constraintTop_toTopOf="@id/status_quote_inline_avatar"
tools:text="Display Name" />
<TextView
android:id="@+id/status_quote_inline_username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:importantForAccessibility="no"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="@id/status_quote_inline_avatar"
app:layout_constraintStart_toEndOf="@id/status_quote_inline_display_name"
app:layout_constraintTop_toTopOf="@id/status_quote_inline_avatar"
tools:text="\@ars42525\@odakyu.app" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_quote_inline_content_warning_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_quote_inline_avatar"
tools:text="CW"
tools:visibility="visible" />
<ToggleButton
android:id="@+id/status_quote_inline_content_warning_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/content_warning_button"
android:importantForAccessibility="no"
android:minWidth="150dp"
android:minHeight="0dp"
android:paddingLeft="16dp"
android:paddingTop="4dp"
android:paddingRight="16dp"
android:paddingBottom="4dp"
android:textAllCaps="true"
android:textOff="@string/status_content_warning_show_more"
android:textOn="@string/status_content_warning_show_less"
android:textSize="?attr/status_text_medium"
android:visibility="gone"
app:layout_constraintStart_toEndOf="@id/status_quote_inline_content_warning_description"
app:layout_constraintTop_toTopOf="@id/status_quote_inline_content_warning_description"
tools:visibility="visible" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/status_quote_inline_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:focusable="true"
android:importantForAccessibility="no"
android:lineSpacingMultiplier="1.1"
android:maxLines="8"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_quote_inline_content_warning_button"
tools:text="This is a status" />
<TextView
android:id="@+id/status_quote_inline_media"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="@string/status_quote_media"
android:textColor="?android:textColorHint"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/status_quote_inline_content"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -46,6 +46,7 @@
<string name="status_content_warning_show_less">続きを隠す</string>
<string name="status_content_show_more">続きを読む</string>
<string name="status_content_show_less">閉じる</string>
<string name="status_quote_media">このトゥートには%d件のメディアが存在します。</string>
<string name="message_empty">何もありません。</string>
<string name="footer_empty">現在トゥートはありません。更新するにはプルダウンしてください!</string>
<string name="notification_reblog_format">%sさんがトゥートをブーストしました</string>
@ -57,6 +58,7 @@
<string name="action_reply">返信</string>
<string name="action_reblog">ブースト</string>
<string name="action_favourite">お気に入り</string>
<string name="action_quote">引用</string>
<string name="action_more">その他</string>
<string name="action_compose">新規投稿</string>
<string name="action_login">Mastodonでログイン</string>

View File

@ -37,6 +37,8 @@
<item name="status_reblog_direct_drawable">@drawable/reblog_direct_dark</item>
<item name="status_favourite_active_drawable">@drawable/favourite_active_dark</item>
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_dark</item>
<item name="status_quote_drawable">@drawable/ic_quote_24dp</item>
<item name="status_quote_disabled_drawable">@drawable/ic_quote_disabled_24dp</item>
<item name="content_warning_button">@drawable/toggle_small</item>
<item name="sensitive_media_warning_background_color">@color/color_background_dark</item>
<item name="media_preview_unloaded_drawable">@drawable/media_preview_unloaded_dark</item>

View File

@ -22,6 +22,8 @@
<attr name="status_reblog_direct_drawable" format="reference" />
<attr name="status_favourite_active_drawable" format="reference" />
<attr name="status_favourite_inactive_drawable" format="reference" />
<attr name="status_quote_drawable" format="reference" />
<attr name="status_quote_disabled_drawable" format="reference" />
<attr name="content_warning_button" format="reference" />
<attr name="sensitive_media_warning_background_color" format="reference|color" />
<attr name="media_preview_unloaded_drawable" format="reference" />

View File

@ -51,6 +51,7 @@
<string name="status_content_warning_show_less">Show Less</string>
<string name="status_content_show_more">Expand</string>
<string name="status_content_show_less">Collapse</string>
<string name="status_quote_media">This toot has %d media(s).</string>
<string name="message_empty">Nothing here.</string>
<string name="footer_empty">Nothing here. Pull down to refresh!</string>
@ -124,6 +125,7 @@
<string name="action_open_reblogger">Open boost author</string>
<string name="action_open_reblogged_by">Show boosts</string>
<string name="action_open_faved_by">Show favorites</string>
<string name="action_quote">Quote</string>
<string name="title_hashtags_dialog">Hashtags</string>
<string name="title_mentions_dialog">Mentions</string>
@ -321,6 +323,7 @@
<string name="pref_title_alway_open_spoiler">Always expand toots marked with content warnings</string>
<string name="title_media">Media</string>
<string name="replying_to">Replying to @%s</string>
<string name="quote_to">Quote : %s</string>
<string name="load_more_placeholder_text">load more</string>
<string name="pref_title_public_filter_keywords">Public timelines</string>

View File

@ -87,6 +87,8 @@
<item name="status_reblog_direct_drawable">@drawable/reblog_direct_light</item>
<item name="status_favourite_active_drawable">@drawable/favourite_active_light</item>
<item name="status_favourite_inactive_drawable">@drawable/favourite_inactive_light</item>
<item name="status_quote_drawable">@drawable/ic_quote_24dp</item>
<item name="status_quote_disabled_drawable">@drawable/ic_quote_disabled_24dp</item>
<item name="content_warning_button">@drawable/toggle_small_light</item>
<item name="sensitive_media_warning_background_color">
@color/sensitive_media_warning_background_light

View File

@ -316,7 +316,8 @@ class TimelineRepositoryTest {
reblog = null,
url = "http://example.com/statuses/$id",
poll = null,
card = null
card = null,
quote = null
)
}