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:
parent
d64573de8c
commit
f1c71de19a
@ -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);
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user