Tab customization & direct messages tab (#1012)
* custom tabs * custom tabs interface * implement custom tab functionality * add database migration * fix bugs, improve ThemeUtils nullability handling * implement conversationsfragment * setup ConversationViewHolder * implement favs * add button functionality * revert 10.json * revert item_status_notification.xml * implement more menu, replying, fix stuff, clean up * fix tests * fix bug with expanding statuses * min and max number of tabs * settings support, fix bugs * database migration * fix scrolling to top after refresh * fix bugs * fix warning in item_conversation
This commit is contained in:
parent
adf573646e
commit
e371fa0e24
75 changed files with 3663 additions and 296 deletions
|
|
@ -0,0 +1,45 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.keylesspalace.tusky.util.NetworkState
|
||||
import com.keylesspalace.tusky.util.Status
|
||||
import com.keylesspalace.tusky.util.visible
|
||||
import kotlinx.android.synthetic.main.item_network_state.view.*
|
||||
|
||||
class NetworkStateViewHolder(itemView: View,
|
||||
private val retryCallback: () -> Unit)
|
||||
: RecyclerView.ViewHolder(itemView) {
|
||||
|
||||
fun setUpWithNetworkState(state: NetworkState?, fullScreen: Boolean) {
|
||||
itemView.progressBar.visible(state?.status == Status.RUNNING)
|
||||
itemView.retryButton.visible(state?.status == Status.FAILED)
|
||||
itemView.errorMsg.visible(state?.msg != null)
|
||||
itemView.errorMsg.text = state?.msg
|
||||
itemView.retryButton.setOnClickListener {
|
||||
retryCallback()
|
||||
}
|
||||
if(fullScreen) {
|
||||
itemView.layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
} else {
|
||||
itemView.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ public final class PlaceholderViewHolder extends RecyclerView.ViewHolder {
|
|||
PlaceholderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
loadMoreButton = itemView.findViewById(R.id.button_load_more);
|
||||
progressBar = itemView.findViewById(R.id.progress_bar);
|
||||
progressBar = itemView.findViewById(R.id.progressBar);
|
||||
}
|
||||
|
||||
public void setup(final StatusActionListener listener, boolean progress) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import java.lang.CharSequence;
|
|||
import at.connyduck.sparkbutton.SparkButton;
|
||||
import at.connyduck.sparkbutton.SparkEventListener;
|
||||
|
||||
abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private TextView displayName;
|
||||
private TextView username;
|
||||
|
|
@ -54,23 +54,23 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
private ImageButton moreButton;
|
||||
private boolean favourited;
|
||||
private boolean reblogged;
|
||||
private MediaPreviewImageView[] mediaPreviews;
|
||||
protected MediaPreviewImageView[] mediaPreviews;
|
||||
private ImageView[] mediaOverlays;
|
||||
private TextView sensitiveMediaWarning;
|
||||
private View sensitiveMediaShow;
|
||||
private TextView mediaLabel;
|
||||
protected TextView mediaLabel;
|
||||
private ToggleButton contentWarningButton;
|
||||
|
||||
ImageView avatar;
|
||||
TextView timestampInfo;
|
||||
TextView content;
|
||||
TextView contentWarningDescription;
|
||||
public ImageView avatar;
|
||||
public TextView timestampInfo;
|
||||
public TextView content;
|
||||
public TextView contentWarningDescription;
|
||||
|
||||
private boolean useAbsoluteTime;
|
||||
private SimpleDateFormat shortSdf;
|
||||
private SimpleDateFormat longSdf;
|
||||
|
||||
StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||
protected StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||
super(itemView);
|
||||
displayName = itemView.findViewById(R.id.status_display_name);
|
||||
username = itemView.findViewById(R.id.status_username);
|
||||
|
|
@ -108,28 +108,30 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
protected abstract int getMediaPreviewHeight(Context context);
|
||||
|
||||
private void setDisplayName(String name, List<Emoji> customEmojis) {
|
||||
protected void setDisplayName(String name, List<Emoji> customEmojis) {
|
||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(name, customEmojis, displayName);
|
||||
displayName.setText(emojifiedName);
|
||||
}
|
||||
|
||||
private void setUsername(String name) {
|
||||
protected 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 setSpoilerAndContent(StatusViewData.Concrete status,
|
||||
final StatusActionListener listener) {
|
||||
if (status.getSpoilerText() == null || status.getSpoilerText().isEmpty()) {
|
||||
protected void setSpoilerAndContent(boolean expanded,
|
||||
@NonNull Spanned content,
|
||||
@Nullable String spoilerText,
|
||||
@Nullable Status.Mention[] mentions,
|
||||
@NonNull List<Emoji> emojis,
|
||||
final StatusActionListener listener) {
|
||||
if (TextUtils.isEmpty(spoilerText)) {
|
||||
contentWarningDescription.setVisibility(View.GONE);
|
||||
contentWarningButton.setVisibility(View.GONE);
|
||||
this.setTextVisible(true, status, listener);
|
||||
this.setTextVisible(true, content, mentions, emojis, listener);
|
||||
} else {
|
||||
boolean expanded = status.isExpanded();
|
||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(
|
||||
status.getSpoilerText(), status.getStatusEmojis(), contentWarningDescription);
|
||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
||||
contentWarningDescription.setText(emojiSpoiler);
|
||||
contentWarningDescription.setVisibility(View.VISIBLE);
|
||||
contentWarningButton.setVisibility(View.VISIBLE);
|
||||
|
|
@ -139,18 +141,19 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
listener.onExpandedChange(isChecked, getAdapterPosition());
|
||||
}
|
||||
this.setTextVisible(isChecked, status, listener);
|
||||
this.setTextVisible(isChecked, content, mentions, emojis, listener);
|
||||
});
|
||||
this.setTextVisible(expanded, status, listener);
|
||||
this.setTextVisible(expanded, content, mentions, emojis, listener);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTextVisible(boolean expanded, StatusViewData.Concrete status,
|
||||
private void setTextVisible(boolean expanded,
|
||||
Spanned content,
|
||||
Status.Mention[] mentions,
|
||||
List<Emoji> emojis,
|
||||
final StatusActionListener listener) {
|
||||
Status.Mention[] mentions = status.getMentions();
|
||||
if (expanded) {
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(
|
||||
status.getContent(), status.getStatusEmojis(), this.content);
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener);
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
||||
|
|
@ -162,7 +165,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
protected void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
if (TextUtils.isEmpty(url)) {
|
||||
avatar.setImageResource(R.drawable.avatar_default);
|
||||
} else {
|
||||
|
|
@ -219,7 +222,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setIsReply(boolean isReply) {
|
||||
protected void setIsReply(boolean isReply) {
|
||||
if (isReply) {
|
||||
replyButton.setImageResource(R.drawable.ic_reply_all_24dp);
|
||||
} else {
|
||||
|
|
@ -265,13 +268,13 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setFavourited(boolean favourited) {
|
||||
protected void setFavourited(boolean favourited) {
|
||||
this.favourited = favourited;
|
||||
favouriteButton.setChecked(favourited);
|
||||
}
|
||||
|
||||
private void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener, boolean showingContent) {
|
||||
protected void setMediaPreviews(final List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener, boolean showingContent) {
|
||||
|
||||
Context context = itemView.getContext();
|
||||
|
||||
|
|
@ -406,8 +409,8 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
private void setMediaLabel(List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener) {
|
||||
protected void setMediaLabel(List<Attachment> attachments, boolean sensitive,
|
||||
final StatusActionListener listener) {
|
||||
if (attachments.size() == 0) {
|
||||
mediaLabel.setVisibility(View.GONE);
|
||||
return;
|
||||
|
|
@ -432,12 +435,12 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
mediaLabel.setOnClickListener(v -> listener.onViewMedia(getAdapterPosition(), 0, null));
|
||||
}
|
||||
|
||||
private void hideSensitiveMediaWarning() {
|
||||
protected void hideSensitiveMediaWarning() {
|
||||
sensitiveMediaWarning.setVisibility(View.GONE);
|
||||
sensitiveMediaShow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void setupButtons(final StatusActionListener listener, final String accountId) {
|
||||
protected void setupButtons(final StatusActionListener listener, final String accountId) {
|
||||
/* Originally position was passed through to all these listeners, but it caused several
|
||||
* bugs where other statuses in the list would be removed or added and cause the position
|
||||
* here to become outdated. So, getting the adapter position at the time the listener is
|
||||
|
|
@ -449,23 +452,25 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
listener.onReply(position);
|
||||
}
|
||||
});
|
||||
reblogButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
if(reblogButton != null) {
|
||||
reblogButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onReblog(!reblogged, position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||
}
|
||||
@Override
|
||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
||||
}
|
||||
});
|
||||
}
|
||||
favouriteButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
|
|
@ -503,8 +508,8 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
itemView.setOnClickListener(viewThreadListener);
|
||||
}
|
||||
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
setDisplayName(status.getUserFullName(), status.getAccountEmojis());
|
||||
setUsername(status.getNickname());
|
||||
setCreatedAt(status.getCreatedAt());
|
||||
|
|
@ -535,7 +540,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setupButtons(listener, status.getSenderId());
|
||||
setRebloggingEnabled(status.getRebloggingEnabled(), status.getVisibility());
|
||||
|
||||
setSpoilerAndContent(status, listener);
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,8 +130,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(final StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
protected void setAvatar(String url, @Nullable String rebloggedUrl) {
|
||||
super.setAvatar(url, rebloggedUrl);
|
||||
|
||||
Context context = avatar.getContext();
|
||||
|
|
@ -75,8 +75,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
|||
}
|
||||
|
||||
@Override
|
||||
void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
protected void setupWithStatus(StatusViewData.Concrete status, final StatusActionListener listener,
|
||||
boolean mediaPreviewEnabled) {
|
||||
if(status == null) {
|
||||
showContent(false);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
/* Copyright 2019 Conny Duck
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.TabData
|
||||
import com.keylesspalace.tusky.util.ThemeUtils
|
||||
import kotlinx.android.synthetic.main.item_tab_preference.view.*
|
||||
|
||||
interface ItemInteractionListener {
|
||||
fun onTabAdded(tab: TabData)
|
||||
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
|
||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
||||
}
|
||||
|
||||
class TabAdapter(var data: List<TabData>,
|
||||
val small: Boolean = false,
|
||||
val listener: ItemInteractionListener? = null) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
||||
|
||||
fun updateData(newData: List<TabData>) {
|
||||
this.data = newData
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val layoutId = if(small) {
|
||||
R.layout.item_tab_preference_small
|
||||
} else {
|
||||
R.layout.item_tab_preference
|
||||
}
|
||||
val view = LayoutInflater.from(parent.context).inflate(layoutId, parent, false)
|
||||
return ViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.itemView.textView.setText(data[position].text)
|
||||
val iconDrawable = ThemeUtils.getTintedDrawable(holder.itemView.context, data[position].icon, android.R.attr.textColorSecondary)
|
||||
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
|
||||
if(small) {
|
||||
holder.itemView.textView.setOnClickListener {
|
||||
listener?.onTabAdded(data[position])
|
||||
}
|
||||
}
|
||||
holder.itemView.imageView?.setOnTouchListener { _, event ->
|
||||
if(event.action == MotionEvent.ACTION_DOWN) {
|
||||
listener?.onStartDrag(holder)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue