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:
Konrad Pozniak 2019-02-12 19:22:37 +01:00 committed by GitHub
commit e371fa0e24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
75 changed files with 3663 additions and 296 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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