1
0
mirror of https://github.com/tuskyapp/Tusky synced 2025-02-09 12:48:43 +01:00

Add support for expandable content to notifications too

This commit is contained in:
HellPie 2018-08-31 05:20:35 +02:00 committed by HellPie
parent d64573de8c
commit f1c71de19a
2 changed files with 159 additions and 11 deletions

View File

@ -25,6 +25,7 @@ import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.text.BidiFormatter;
import android.support.v7.widget.RecyclerView;
import android.text.InputFilter;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
@ -244,6 +245,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
void onExpandedChange(boolean expanded, int position);
/**
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
* status content is interacted with.
*
* @param isCollapsed Whether the status content is shown in a collapsed state or fully.
* @param position The position of the status in the list.
*/
void onNotificationContentCollapsedChange(boolean isCollapsed, int position);
}
private static class FollowViewHolder extends RecyclerView.ViewHolder {
@ -309,6 +318,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private final ImageView notificationAvatar;
private final TextView contentWarningDescriptionTextView;
private final ToggleButton contentWarningButton;
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
private String accountId;
private String notificationId;
@ -337,6 +347,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
message.setOnClickListener(this);
statusContent.setOnClickListener(this);
contentWarningButton.setOnCheckedChangeListener(this);
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
}
private void showNotificationContent(boolean show) {
@ -346,7 +358,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
statusContent.setVisibility(show ? View.VISIBLE : View.GONE);
statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
notificationAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
}
private void setDisplayName(String name, List<Emoji> emojis) {
@ -488,11 +499,58 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
Spanned content = statusViewData.getContent();
List<Emoji> emojis = statusViewData.getStatusEmojis();
if(contentCollapseButton != null && statusViewData.isCollapsible() && (notificationViewData.isExpanded() || !hasSpoiler)) {
contentCollapseButton.setOnCheckedChangeListener((buttonView, isChecked) -> {
int position = getAdapterPosition();
if(position != RecyclerView.NO_POSITION && notificationActionListener != null) {
notificationActionListener.onNotificationContentCollapsedChange(isChecked, position);
}
});
contentCollapseButton.setVisibility(View.VISIBLE);
if(statusViewData.isCollapsed()) {
contentCollapseButton.setChecked(true);
statusContent.setFilters(new InputFilter[]{(source, start, end, dest, dstart, dend) -> {
// Code imported from InputFilter.LengthFilter
// https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175
// Changes:
// - After the text it adds and ellipsis to make it feel like the text continues
// - Max value is 500 rather than a variable
// - Trim invisible characters off the end of the 500-limited string
// - Slimmed code for saving LOCs
int keep = 500 - (dest.length() - (dend - dstart));
if(keep <= 0) return "";
if(keep >= end - start) return null; // keep original
keep += start;
while(Character.isWhitespace(source.charAt(keep - 1))) {
--keep;
if(keep == start) return "";
}
if(Character.isHighSurrogate(source.charAt(keep - 1))) {
--keep;
if(keep == start) return "";
}
return source.subSequence(start, keep) + "";
}});
} else {
contentCollapseButton.setChecked(false);
statusContent.setFilters(new InputFilter[]{});
}
} else if(contentCollapseButton != null) {
contentCollapseButton.setVisibility(View.GONE);
statusContent.setFilters(new InputFilter[]{});
}
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent);
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
Spanned emojifiedContentWarning =
CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView);
contentWarningDescriptionTextView.setText(emojifiedContentWarning);

View File

@ -40,6 +40,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.ToggleButton;
import com.keylesspalace.tusky.MainActivity;
import com.keylesspalace.tusky.R;
@ -136,6 +137,7 @@ public class NotificationsFragment extends SFragment implements
private String bottomId;
private String topId;
private boolean alwaysShowSensitiveMedia;
private boolean collapseLongStatusContent;
@Override
protected TimelineCases timelineCases() {
@ -149,7 +151,11 @@ public class NotificationsFragment extends SFragment implements
public NotificationViewData apply(Either<Placeholder, Notification> input) {
if (input.isRight()) {
Notification notification = input.getAsRight();
return ViewDataUtils.notificationToViewData(notification, alwaysShowSensitiveMedia);
return ViewDataUtils.notificationToViewData(
notification,
alwaysShowSensitiveMedia,
collapseLongStatusContent
);
} else {
return new NotificationViewData.Placeholder(false);
}
@ -194,6 +200,7 @@ public class NotificationsFragment extends SFragment implements
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
getActivity());
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
recyclerView.setAdapter(adapter);
@ -491,9 +498,82 @@ public class NotificationsFragment extends SFragment implements
}
}
/**
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
* status content is interacted with.
*
* @param isCollapsed Whether the status content is shown in a collapsed state or fully.
* @param position The position of the status in the list.
*/
@Override
public void onContentCollapsedChange(boolean isCollapsed, int position) {
// TODO: Implement this method.
if(position < 0 || position >= notifications.size()) {
Log.e(TAG, String.format("Tried to access out of bounds status position: %d of %d", position, notifications.size() - 1));
return;
}
NotificationViewData notification = notifications.getPairedItem(position);
if(!(notification instanceof NotificationViewData.Concrete)) {
if(notification == null) {
Log.e(TAG, String.format(
"Tried to access notification but got null at position: %d of %d",
position,
notifications.size() - 1)
);
} else {
Log.e(TAG, String.format(
"Expected NotificationViewData.Concrete, got %s instead at position: %d of %d",
notification.getClass().getSimpleName(),
position,
notifications.size() - 1
));
}
return;
}
StatusViewData.Concrete status = ((NotificationViewData.Concrete) notification).getStatusViewData();
if(status == null) {
Log.e(TAG, String.format(
"Tried to access status in notification but got null at position: %d of %d",
position,
notifications.size() - 1)
);
return;
}
StatusViewData.Concrete updatedStatus = new StatusViewData.Builder(status)
.setCollapsed(isCollapsed)
.createStatusViewData();
NotificationViewData.Concrete concreteNotification = (NotificationViewData.Concrete) notification;
NotificationViewData updatedNotification = new NotificationViewData.Concrete(
concreteNotification.getType(),
concreteNotification.getId(),
concreteNotification.getAccount(),
updatedStatus,
concreteNotification.isExpanded()
);
notifications.setPairedItem(position, updatedNotification);
adapter.updateItemWithNotify(position, updatedNotification, false);
// Since we cannot notify to the RecyclerView right away because it may be scrolling
// we run this when the RecyclerView is done doing measurements and other calculations.
// To test this is not bs: try getting a notification while scrolling, without wrapping
// notifyItemChanged in a .post() call. App will crash.
recyclerView.post(() -> adapter.notifyItemChanged(position, notification));
}
/**
* Called when the status {@link ToggleButton} responsible for collapsing long
* status content is interacted with.
*
* @param isCollapsed Whether the status content is shown in a collapsed state or fully.
* @param position The position of the status in the list.
*/
@Override
public void onNotificationContentCollapsedChange(boolean isCollapsed, int position) {
onContentCollapsedChange(isCollapsed, position);
}
@Override
@ -533,6 +613,10 @@ public class NotificationsFragment extends SFragment implements
}
break;
}
case "collapseLongStatuses":
collapseLongStatusContent = sharedPreferences.getBoolean("collapseLongStatuses", true);
fullyRefresh();
break;
}
}
@ -560,12 +644,18 @@ public class NotificationsFragment extends SFragment implements
// already loaded everything
return;
}
Either<Placeholder, Notification> last = notifications.get(notifications.size() - 1);
if (last.isRight()) {
notifications.add(Either.left(Placeholder.getInstance()));
NotificationViewData viewData = new NotificationViewData.Placeholder(true);
notifications.setPairedItem(notifications.size() - 1, viewData);
recyclerView.post(() -> adapter.addItems(Collections.singletonList(viewData)));
// Check for out-of-bounds when loading
// This is required to allow full-timeline reloads of collapsible statuses when the settings
// change.
if(notifications.size() > 0) {
Either<Placeholder, Notification> last = notifications.get(notifications.size() - 1);
if(last.isRight()) {
notifications.add(Either.left(Placeholder.getInstance()));
NotificationViewData viewData = new NotificationViewData.Placeholder(true);
notifications.setPairedItem(notifications.size() - 1, viewData);
recyclerView.post(() -> adapter.addItems(Collections.singletonList(viewData)));
}
}
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1);