mirror of
https://github.com/tuskyapp/Tusky
synced 2025-02-10 07:20:40 +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.content.ContextCompat;
|
||||||
import android.support.v4.text.BidiFormatter;
|
import android.support.v4.text.BidiFormatter;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.InputFilter;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
@ -244,6 +245,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||||||
|
|
||||||
void onExpandedChange(boolean expanded, int position);
|
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 {
|
private static class FollowViewHolder extends RecyclerView.ViewHolder {
|
||||||
@ -309,6 +318,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||||||
private final ImageView notificationAvatar;
|
private final ImageView notificationAvatar;
|
||||||
private final TextView contentWarningDescriptionTextView;
|
private final TextView contentWarningDescriptionTextView;
|
||||||
private final ToggleButton contentWarningButton;
|
private final ToggleButton contentWarningButton;
|
||||||
|
private final ToggleButton contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
|
||||||
|
|
||||||
private String accountId;
|
private String accountId;
|
||||||
private String notificationId;
|
private String notificationId;
|
||||||
@ -337,6 +347,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||||||
message.setOnClickListener(this);
|
message.setOnClickListener(this);
|
||||||
statusContent.setOnClickListener(this);
|
statusContent.setOnClickListener(this);
|
||||||
contentWarningButton.setOnCheckedChangeListener(this);
|
contentWarningButton.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
|
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showNotificationContent(boolean show) {
|
private void showNotificationContent(boolean show) {
|
||||||
@ -346,7 +358,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||||||
statusContent.setVisibility(show ? View.VISIBLE : View.GONE);
|
statusContent.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
statusAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
notificationAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
notificationAvatar.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setDisplayName(String name, List<Emoji> emojis) {
|
private void setDisplayName(String name, List<Emoji> emojis) {
|
||||||
@ -488,11 +499,58 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
|||||||
Spanned content = statusViewData.getContent();
|
Spanned content = statusViewData.getContent();
|
||||||
List<Emoji> emojis = statusViewData.getStatusEmojis();
|
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);
|
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, statusContent);
|
||||||
|
|
||||||
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
|
LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener);
|
||||||
|
|
||||||
|
|
||||||
Spanned emojifiedContentWarning =
|
Spanned emojifiedContentWarning =
|
||||||
CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView);
|
CustomEmojiHelper.emojifyString(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView);
|
||||||
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
contentWarningDescriptionTextView.setText(emojifiedContentWarning);
|
||||||
|
@ -40,6 +40,7 @@ import android.view.View;
|
|||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.ToggleButton;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
@ -136,6 +137,7 @@ public class NotificationsFragment extends SFragment implements
|
|||||||
private String bottomId;
|
private String bottomId;
|
||||||
private String topId;
|
private String topId;
|
||||||
private boolean alwaysShowSensitiveMedia;
|
private boolean alwaysShowSensitiveMedia;
|
||||||
|
private boolean collapseLongStatusContent;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TimelineCases timelineCases() {
|
protected TimelineCases timelineCases() {
|
||||||
@ -149,7 +151,11 @@ public class NotificationsFragment extends SFragment implements
|
|||||||
public NotificationViewData apply(Either<Placeholder, Notification> input) {
|
public NotificationViewData apply(Either<Placeholder, Notification> input) {
|
||||||
if (input.isRight()) {
|
if (input.isRight()) {
|
||||||
Notification notification = input.getAsRight();
|
Notification notification = input.getAsRight();
|
||||||
return ViewDataUtils.notificationToViewData(notification, alwaysShowSensitiveMedia);
|
return ViewDataUtils.notificationToViewData(
|
||||||
|
notification,
|
||||||
|
alwaysShowSensitiveMedia,
|
||||||
|
collapseLongStatusContent
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
return new NotificationViewData.Placeholder(false);
|
return new NotificationViewData.Placeholder(false);
|
||||||
}
|
}
|
||||||
@ -194,6 +200,7 @@ public class NotificationsFragment extends SFragment implements
|
|||||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||||
getActivity());
|
getActivity());
|
||||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||||
|
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
||||||
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
recyclerView.setAdapter(adapter);
|
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
|
@Override
|
||||||
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
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
|
@Override
|
||||||
@ -533,6 +613,10 @@ public class NotificationsFragment extends SFragment implements
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "collapseLongStatuses":
|
||||||
|
collapseLongStatusContent = sharedPreferences.getBoolean("collapseLongStatuses", true);
|
||||||
|
fullyRefresh();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -560,12 +644,18 @@ public class NotificationsFragment extends SFragment implements
|
|||||||
// already loaded everything
|
// already loaded everything
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Either<Placeholder, Notification> last = notifications.get(notifications.size() - 1);
|
|
||||||
if (last.isRight()) {
|
// Check for out-of-bounds when loading
|
||||||
notifications.add(Either.left(Placeholder.getInstance()));
|
// This is required to allow full-timeline reloads of collapsible statuses when the settings
|
||||||
NotificationViewData viewData = new NotificationViewData.Placeholder(true);
|
// change.
|
||||||
notifications.setPairedItem(notifications.size() - 1, viewData);
|
if(notifications.size() > 0) {
|
||||||
recyclerView.post(() -> adapter.addItems(Collections.singletonList(viewData)));
|
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);
|
sendFetchNotificationsRequest(bottomId, null, FetchEnd.BOTTOM, -1);
|
||||||
|
Loading…
x
Reference in New Issue
Block a user