Add severed_relationships notifications (AND-174)

This commit is contained in:
Grishka 2024-09-21 10:40:04 +03:00
parent 9533b4f45d
commit 16e2632d9b
10 changed files with 247 additions and 7 deletions

View File

@ -11,6 +11,7 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import org.joinmastodon.android.ui.displayitems.InlineStatusStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationWithButtonStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.parceler.Parcels;
@ -35,7 +36,10 @@ public abstract class BaseNotificationsListFragment extends BaseStatusListFragme
else
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID);
if(n.notification.type==NotificationType.SEVERED_RELATIONSHIPS)
titleItem=new NotificationWithButtonStatusDisplayItem(n.getID(), this, n, accountID);
else
titleItem=new NotificationHeaderStatusDisplayItem(n.getID(), this, n, accountID);
}
if(n.status!=null){
if(titleItem!=null && n.notification.type!=NotificationType.STATUS){

View File

@ -1,5 +1,7 @@
package org.joinmastodon.android.model;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
@ -14,10 +16,9 @@ public class Notification extends BaseModel implements DisplayItemsParent{
public NotificationType type;
@RequiredField
public Instant createdAt;
@RequiredField
public Account account;
public Status status;
public RelationshipSeveranceEvent event;
@Override
public void postprocess() throws ObjectValidationException{
@ -25,6 +26,17 @@ public class Notification extends BaseModel implements DisplayItemsParent{
account.postprocess();
if(status!=null)
status.postprocess();
if(event!=null){
try{
event.postprocess();
}catch(ObjectValidationException x){
Log.w("Notification", x);
event=null;
}
}
if(type!=NotificationType.SEVERED_RELATIONSHIPS && account==null){
throw new ObjectValidationException("account must be present for type "+type);
}
}
@Override

View File

@ -1,5 +1,8 @@
package org.joinmastodon.android.model;
import android.util.Log;
import org.joinmastodon.android.api.ObjectValidationException;
import org.joinmastodon.android.api.RequiredField;
import java.time.Instant;
@ -18,7 +21,23 @@ public class NotificationGroup extends BaseModel{
@RequiredField
public List<String> sampleAccountIds;
public String statusId;
// TODO report
// TODO event
public RelationshipSeveranceEvent event;
// TODO moderation_warning
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
if(event!=null){
try{
event.postprocess();
}catch(ObjectValidationException x){
Log.w("Notification", x);
event=null;
}
}
if(type!=NotificationType.SEVERED_RELATIONSHIPS && sampleAccountIds.isEmpty()){
throw new ObjectValidationException("sample_account_ids must be present for type "+type);
}
}
}

View File

@ -20,7 +20,9 @@ public enum NotificationType{
@SerializedName("status")
STATUS,
@SerializedName("update")
UPDATE;
UPDATE,
@SerializedName("severed_relationships")
SEVERED_RELATIONSHIPS;
public boolean canBeGrouped(){
return this==REBLOG || this==FAVORITE;

View File

@ -0,0 +1,30 @@
package org.joinmastodon.android.model;
import com.google.gson.annotations.SerializedName;
import org.joinmastodon.android.api.RequiredField;
import org.parceler.Parcel;
import java.time.Instant;
@Parcel
public class RelationshipSeveranceEvent extends BaseModel{
public String id;
@RequiredField
public Type type;
public boolean purged;
@RequiredField
public String targetName;
public int followersCount;
public int followingCount;
public Instant createdAt;
public enum Type{
@SerializedName("domain_block")
DOMAIN_BLOCK,
@SerializedName("user_domain_block")
USER_DOMAIN_BLOCK,
@SerializedName("account_suspension")
ACCOUNT_SUSPENSION
}
}

View File

@ -0,0 +1,102 @@
package org.joinmastodon.android.ui.displayitems;
import android.app.Activity;
import android.content.res.ColorStateList;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.style.TypefaceSpan;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.model.NotificationType;
import org.joinmastodon.android.model.RelationshipSeveranceEvent;
import org.joinmastodon.android.model.viewmodel.NotificationViewModel;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Map;
public class NotificationWithButtonStatusDisplayItem extends StatusDisplayItem{
private final NotificationViewModel notification;
private CharSequence text;
private String buttonText;
private Runnable buttonAction;
public NotificationWithButtonStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, NotificationViewModel notification, String accountID){
super(parentID, parentFragment);
this.notification=notification;
if(notification.notification.type==NotificationType.SEVERED_RELATIONSHIPS){
RelationshipSeveranceEvent event=notification.notification.event;
String localDomain=AccountSessionManager.get(accountID).domain;
if(event!=null){
text=switch(event.type){
case ACCOUNT_SUSPENSION -> replacePlaceholdersWithBoldStrings(parentFragment.getString(R.string.relationship_severance_account_suspension,
"{{localDomain}}", "{{target}}"), Map.of("localDomain", localDomain, "target", event.targetName));
case DOMAIN_BLOCK -> replacePlaceholdersWithBoldStrings(parentFragment.getString(R.string.relationship_severance_domain_block,
"{{localDomain}}", "{{target}}", event.followersCount, parentFragment.getResources().getQuantityString(R.plurals.x_accounts, event.followingCount, event.followingCount)),
Map.of("localDomain", localDomain, "target", event.targetName));
case USER_DOMAIN_BLOCK -> replacePlaceholdersWithBoldStrings(parentFragment.getString(R.string.relationship_severance_user_domain_block,
"{{target}}", event.followersCount, parentFragment.getResources().getQuantityString(R.plurals.x_accounts, event.followingCount, event.followingCount)),
Map.of("target", event.targetName));
};
}else{
text="???";
}
buttonText=parentFragment.getString(R.string.relationship_severance_learn_more);
buttonAction=()->UiUtils.launchWebBrowser(parentFragment.getActivity(), "https://"+localDomain+"/severed_relationships");
}
}
private SpannableStringBuilder replacePlaceholdersWithBoldStrings(String in, Map<String, String> replacements){
SpannableStringBuilder ssb=new SpannableStringBuilder(in);
for(Map.Entry<String, String> e:replacements.entrySet()){
String placeholder="{{"+e.getKey()+"}}";
int index=ssb.toString().indexOf(placeholder);
if(index==-1)
continue;
ssb.replace(index, index+placeholder.length(), e.getValue());
ssb.setSpan(new TypefaceSpan("sans-serif-medium"), index, index+e.getValue().length(), 0);
}
return ssb;
}
@Override
public Type getType(){
return Type.NOTIFICATION_WITH_BUTTON;
}
public static class Holder extends StatusDisplayItem.Holder<NotificationWithButtonStatusDisplayItem>{
private final ImageView icon;
private final TextView text;
private final Button button;
public Holder(Activity activity, ViewGroup parent){
super(activity, R.layout.display_item_notification_with_button, parent);
icon=findViewById(R.id.icon);
text=findViewById(R.id.text);
button=findViewById(R.id.button);
button.setOnClickListener(v->item.buttonAction.run());
}
@Override
public void onBind(NotificationWithButtonStatusDisplayItem item){
icon.setImageResource(switch(item.notification.notification.type){
case SEVERED_RELATIONSHIPS -> R.drawable.ic_heart_broken_fill1_24px;
default -> throw new IllegalStateException("Unexpected value: " + item.notification.notification.type);
});
icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Outline)));
text.setText(item.text);
button.setText(item.buttonText);
button.setEnabled(item.buttonAction!=null);
}
@Override
public boolean isEnabled(){
return false;
}
}
}

View File

@ -81,6 +81,7 @@ public abstract class StatusDisplayItem{
case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.Holder(activity, parent);
case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent);
case INLINE_STATUS -> new InlineStatusStatusDisplayItem.Holder(activity, parent);
case NOTIFICATION_WITH_BUTTON -> new NotificationWithButtonStatusDisplayItem.Holder(activity, parent);
};
}
@ -226,7 +227,8 @@ public abstract class StatusDisplayItem{
HEADER_CHECKABLE,
NOTIFICATION_HEADER,
FILTER_SPOILER,
INLINE_STATUS
INLINE_STATUS,
NOTIFICATION_WITH_BUTTON
}
public static abstract class Holder<T> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M481,877Q347,742 267.5,659Q188,576 146.5,521Q105,466 92.5,427Q80,388 80,340Q80,248 144,184Q208,120 300,120Q345,120 387,136.5Q429,153 462,184L400,400L520,400L486,735L600,360L480,360L551,148Q576,134 603.5,127Q631,120 660,120Q752,120 816,184Q880,248 880,340Q880,388 867,428Q854,468 812,523.5Q770,579 691,661.5Q612,744 481,877Z"/>
</vector>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingTop="12dp">
<ImageView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="28dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="8dp"
android:importantForAccessibility="no"
android:scaleType="center"
tools:tint="#0f0"
tools:src="@drawable/ic_repeat_24px"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_alignWithParentIfMissing="true"
android:textAppearance="@style/m3_body_medium"
android:textColor="?colorM3OnSurface"
android:minHeight="20dp"
android:gravity="center_vertical"
tools:text="Notification text"/>
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="28dp"
android:layout_below="@id/text"
android:layout_toEndOf="@id/icon"
android:layout_marginStart="-8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_button_m3_text"
android:textColor="?colorM3Primary"
android:paddingHorizontal="8dp"
android:minWidth="0dp"
android:gravity="start|center_vertical"
tools:text="Button text"/>
</RelativeLayout>

View File

@ -798,4 +798,14 @@
<string name="own_poll_ended">Your poll has ended</string>
<string name="user_just_posted">%s just posted</string>
<string name="user_edited_post">%s edited a post you interacted with</string>
<string name="relationship_severance_account_suspension">An admin from %1$s has suspended %2$s, which means you can no longer receive updates from them or interact with them.</string>
<!-- %1$s is your server domain, %2$s is the domain that was blocked, %3$,d is the follower count, %4$s is the `x_accounts` plural string -->
<string name="relationship_severance_domain_block">An admin from %1$s has blocked %2$s, including %3$,d of your followers and %4$s you follow.</string>
<plurals name="x_accounts">
<item quantity="one">%,d account</item>
<item quantity="other">%,d accounts</item>
</plurals>
<!-- %1$s is the domain that was blocked, %2$,d is the follower count, %3$s is the `x_accounts` plural string -->
<string name="relationship_severance_user_domain_block">You have blocked %1$s, removing %2$,d of your followers and %3$s you follow.</string>
<string name="relationship_severance_learn_more">Learn more</string>
</resources>