Pre-reply sheets

This commit is contained in:
Grishka 2023-11-15 18:05:38 +03:00
parent 45cc531eec
commit a438f633be
21 changed files with 503 additions and 40 deletions

View File

@ -3,6 +3,9 @@ package org.joinmastodon.android;
import android.content.Context;
import android.content.SharedPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
public class GlobalUserPreferences{
public static boolean playGifs;
public static boolean useCustomTabs;
@ -13,6 +16,10 @@ public class GlobalUserPreferences{
return MastodonApp.context.getSharedPreferences("global", Context.MODE_PRIVATE);
}
private static SharedPreferences getPreReplyPrefs(){
return MastodonApp.context.getSharedPreferences("pre_reply_sheets", Context.MODE_PRIVATE);
}
public static void load(){
SharedPreferences prefs=getPrefs();
playGifs=prefs.getBoolean("playGifs", true);
@ -36,9 +43,42 @@ public class GlobalUserPreferences{
.apply();
}
public static boolean isOptedOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
if(getPreReplyPrefs().getBoolean("opt_out_"+type, false))
return true;
if(account==null)
return false;
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
return getPreReplyPrefs().getBoolean("opt_out_"+type+"_"+accountKey.toLowerCase(), false);
}
public static void optOutOfPreReplySheet(PreReplySheetType type, Account account, String accountID){
String key;
if(account==null){
key="opt_out_"+type;
}else{
String accountKey=account.acct;
if(!accountKey.contains("@"))
accountKey+="@"+AccountSessionManager.get(accountID).domain;
key="opt_out_"+type+"_"+accountKey.toLowerCase();
}
getPreReplyPrefs().edit().putBoolean(key, true).apply();
}
public static void resetPreReplySheets(){
getPreReplyPrefs().edit().clear().apply();
}
public enum ThemePreference{
AUTO,
LIGHT,
DARK
}
public enum PreReplySheetType{
OLD_POST,
NON_MUTUAL
}
}

View File

@ -14,10 +14,12 @@ import android.view.WindowInsets;
import android.widget.Toolbar;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent;
@ -27,6 +29,8 @@ import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.Translation;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
import org.joinmastodon.android.ui.OldPostPreReplySheet;
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
@ -43,6 +47,8 @@ import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.TypedObjectPool;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@ -106,6 +112,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
for(T s:items){
displayItems.addAll(buildDisplayItems(s));
}
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
}
@Override
@ -127,6 +134,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
if(notify)
adapter.notifyItemRangeInserted(0, offset);
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
}
protected String getMaxID(){
@ -455,6 +463,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
protected void loadRelationships(Set<String> ids){
if(ids.isEmpty())
return;
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
if(ids.isEmpty())
return;
// TODO somehow manage these and cancel outstanding requests on refresh
@ -641,6 +652,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
adapter.notifyDataSetChanged();
}
public void maybeShowPreReplySheet(Status status, Runnable proceed){
Relationship rel=getRelationship(status.account.id);
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
new NonMutualPreReplySheet(getActivity(), notAgain->{
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
proceed.run();
}, status.account).show();
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
new OldPostPreReplySheet(getActivity(), notAgain->{
if(notAgain)
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
proceed.run();
}, status).show();
}else{
proceed.run();
}
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{

View File

@ -210,11 +210,13 @@ public class ThreadFragment extends StatusListFragment{
}
private void openReply(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
args.putBoolean("fromThreadFragment", true);
Nav.go(getActivity(), ComposeFragment.class, args);
maybeShowPreReplySheet(mainStatus, ()->{
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
args.putBoolean("fromThreadFragment", true);
Nav.go(getActivity(), ComposeFragment.class, args);
});
}
public int getSnackbarOffset(){

View File

@ -1,10 +1,9 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.widget.Toast;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
@ -28,7 +27,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick)
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
@ -65,6 +65,11 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
restartUI();
}
private void onResetPreReplySheetsClick(ListItem<?> item){
GlobalUserPreferences.resetPreReplySheets();
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);

View File

@ -5,4 +5,8 @@ package org.joinmastodon.android.model;
*/
public interface DisplayItemsParent{
String getID();
default String getAccountID(){
return null;
}
}

View File

@ -34,6 +34,11 @@ public class Notification extends BaseModel implements DisplayItemsParent{
return id;
}
@Override
public String getAccountID(){
return status!=null ? account.id : null;
}
public enum Type{
@SerializedName("follow")
FOLLOW,

View File

@ -34,10 +34,18 @@ public class SearchResult extends BaseModel implements DisplayItemsParent{
generateID();
}
@Override
public String getID(){
return id;
}
@Override
public String getAccountID(){
if(type==Type.STATUS)
return status.getAccountID();
return null;
}
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();

View File

@ -143,6 +143,11 @@ public class Status extends BaseModel implements DisplayItemsParent{
return id;
}
@Override
public String getAccountID(){
return getContentStatus().account.id;
}
public void update(StatusCountersUpdatedEvent ev){
favouritesCount=ev.favorites;
reblogsCount=ev.reblogs;

View File

@ -0,0 +1,123 @@
package org.joinmastodon.android.ui;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Typeface;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
import android.text.style.TextAppearanceSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class NonMutualPreReplySheet extends PreReplySheet{
@SuppressLint("DefaultLocale")
public NonMutualPreReplySheet(@NonNull Context context, ResultListener resultListener, Account account){
super(context, resultListener);
icon.setImageResource(R.drawable.ic_waving_hand_24px);
title.setText(R.string.non_mutual_sheet_title);
text.setText(R.string.non_mutual_sheet_text);
LinearLayout userInfo=new LinearLayout(context);
userInfo.setOrientation(LinearLayout.HORIZONTAL);
userInfo.setBackgroundResource(R.drawable.bg_user_info);
UiUtils.setAllPaddings(userInfo, 12);
ImageView ava=new ImageView(context);
ava.setScaleType(ImageView.ScaleType.CENTER_CROP);
ava.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
ava.setOutlineProvider(OutlineProviders.roundedRect(12));
ava.setClipToOutline(true);
ava.setForeground(context.getResources().getDrawable(R.drawable.fg_user_info_ava, context.getTheme()));
userInfo.addView(ava, UiUtils.makeLayoutParams(56, 56, 0, 0, 12, 0));
ViewImageLoader.loadWithoutAnimation(ava, context.getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(account.avatarStatic, V.dp(56), V.dp(56)));
LinearLayout nameAndFields=new LinearLayout(context);
nameAndFields.setOrientation(LinearLayout.VERTICAL);
nameAndFields.setMinimumHeight(V.dp(56));
nameAndFields.setGravity(Gravity.CENTER_VERTICAL);
userInfo.addView(nameAndFields, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
TextView name=new TextView(context);
name.setSingleLine();
name.setEllipsize(TextUtils.TruncateAt.END);
name.setTextAppearance(R.style.m3_title_medium);
name.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3OnSurface));
name.setText(account.displayName);
name.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
nameAndFields.addView(name, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
if(account.fields!=null && !account.fields.isEmpty()){
for(AccountField field:account.fields){
LinearLayout fieldView=new LinearLayout(context);
fieldView.setOrientation(LinearLayout.HORIZONTAL);
TextView key=new TextView(context);
key.setTextAppearance(R.style.m3_body_medium);
key.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
key.setSingleLine();
key.setEllipsize(TextUtils.TruncateAt.END);
key.setText(field.name);
key.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
key.setPaddingRelative(0, 0, V.dp(8), 0);
fieldView.addView(key, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
TextView value=new TextView(context);
value.setTextAppearance(R.style.m3_body_medium);
value.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
value.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
value.setSingleLine();
value.setEllipsize(TextUtils.TruncateAt.END);
value.setText(HtmlParser.stripAndRemoveInvisibleSpans(field.value));
value.setGravity(Gravity.CENTER_VERTICAL | Gravity.END);
fieldView.addView(value, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.MATCH_PARENT, 1f));
nameAndFields.addView(fieldView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20)));
}
}else{
TextView username=new TextView(context);
username.setTextAppearance(R.style.m3_body_medium);
username.setTextColor(UiUtils.getThemeColor(context, R.attr.colorM3Secondary));
username.setSingleLine();
username.setEllipsize(TextUtils.TruncateAt.END);
username.setText(account.getDisplayUsername());
username.setGravity(Gravity.CENTER_VERTICAL | Gravity.START);
nameAndFields.addView(username, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20)));
}
contentWrap.addView(userInfo, UiUtils.makeLayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, 0, 0, 0, 8));
for(int i=0;i<3;i++){
View item=context.getSystemService(LayoutInflater.class).inflate(R.layout.item_other_numbered_rule, contentWrap, false);
TextView number=item.findViewById(R.id.number);
number.setText(String.format("%d", i+1));
TextView title=item.findViewById(R.id.title);
TextView text=item.findViewById(R.id.text);
title.setText(switch(i){
case 0 -> R.string.non_mutual_title1;
case 1 -> R.string.non_mutual_title2;
case 2 -> R.string.non_mutual_title3;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
text.setText(switch(i){
case 0 -> R.string.non_mutual_text1;
case 1 -> R.string.non_mutual_text2;
case 2 -> R.string.non_mutual_text3;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
contentWrap.addView(item);
}
}
}

View File

@ -0,0 +1,23 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Status;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import androidx.annotation.NonNull;
public class OldPostPreReplySheet extends PreReplySheet{
public OldPostPreReplySheet(@NonNull Context context, ResultListener resultListener, Status status){
super(context, resultListener);
int months=(int)status.createdAt.atZone(ZoneId.systemDefault()).until(ZonedDateTime.now(), ChronoUnit.MONTHS);
String monthsStr=months>24 ? context.getString(R.string.more_than_two_years) : context.getResources().getQuantityString(R.plurals.x_months, months, months);
title.setText(context.getString(R.string.old_post_sheet_title, monthsStr));
text.setText(R.string.old_post_sheet_text);
icon.setImageResource(R.drawable.ic_history_24px);
}
}

View File

@ -0,0 +1,54 @@
package org.joinmastodon.android.ui;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import androidx.annotation.NonNull;
import me.grishka.appkit.views.BottomSheet;
public abstract class PreReplySheet extends BottomSheet{
protected ImageView icon;
protected TextView title, text;
protected Button gotItButton, dontRemindButton;
protected LinearLayout contentWrap;
public PreReplySheet(@NonNull Context context, ResultListener resultListener){
super(context);
View content=context.getSystemService(LayoutInflater.class).inflate(R.layout.sheet_pre_reply, null);
setContentView(content);
setNavigationBarBackground(new ColorDrawable(UiUtils.alphaBlendColors(UiUtils.getThemeColor(context, R.attr.colorM3Surface),
UiUtils.getThemeColor(context, R.attr.colorM3Primary), 0.05f)), !UiUtils.isDarkTheme());
icon=findViewById(R.id.icon);
title=findViewById(R.id.title);
text=findViewById(R.id.text);
gotItButton=findViewById(R.id.btn_got_it);
dontRemindButton=findViewById(R.id.btn_dont_remind_again);
contentWrap=findViewById(R.id.content_wrap);
gotItButton.setOnClickListener(v->{
dismiss();
resultListener.onButtonClicked(false);
});
dontRemindButton.setOnClickListener(v->{
dismiss();
resultListener.onButtonClicked(true);
});
}
@FunctionalInterface
public interface ResultListener{
void onButtonClicked(boolean notAgain);
}
}

View File

@ -129,10 +129,12 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
private void onReplyClick(View v){
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("replyTo", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
item.parentFragment.maybeShowPreReplySheet(item.status, ()->{
Bundle args=new Bundle();
args.putString("account", item.accountID);
args.putParcelable("replyTo", Parcels.wrap(item.status));
Nav.go(item.parentFragment.getActivity(), ComposeFragment.class, args);
});
}
private void onBoostClick(View v){

View File

@ -115,8 +115,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private final TextView name, timeAndUsername, extraText;
private final ImageView avatar, more;
private final PopupMenu optionsMenu;
private Relationship relationship;
private APIRequest<?> currentRelationshipRequest;
public Holder(Activity activity, ViewGroup parent){
this(activity, R.layout.display_item_header, parent);
@ -140,6 +138,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
optionsMenu.getMenu().setGroupDividerEnabled(true);
optionsMenu.setOnMenuItemClickListener(menuItem->{
Account account=item.user;
Relationship relationship=item.parentFragment.getRelationship(account.id);
int id=menuItem.getItemId();
if(id==R.id.edit){
final Bundle args=new Bundle();
@ -192,7 +191,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
else
progress.dismiss();
}, rel->{
relationship=rel;
item.parentFragment.putRelationship(account.id, rel);
Toast.makeText(activity, activity.getString(rel.following ? R.string.followed_user : rel.requested ? R.string.following_user_requested : R.string.unfollowed_user, account.getDisplayUsername()), Toast.LENGTH_SHORT).show();
});
}else if(id==R.id.bookmark){
@ -235,10 +234,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
more.setVisibility(item.inset ? View.GONE : View.VISIBLE);
avatar.setClickable(!item.inset);
avatar.setContentDescription(item.parentFragment.getString(R.string.avatar_description, item.user.acct));
if(currentRelationshipRequest!=null){
currentRelationshipRequest.cancel();
}
relationship=null;
}
@Override
@ -272,31 +267,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
private void onMoreClick(View v){
updateOptionsMenu();
optionsMenu.show();
if(relationship==null && currentRelationshipRequest==null){
currentRelationshipRequest=new GetAccountRelationships(Collections.singletonList(item.user.id))
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
if(!result.isEmpty()){
relationship=result.get(0);
updateOptionsMenu();
}
currentRelationshipRequest=null;
}
@Override
public void onError(ErrorResponse error){
currentRelationshipRequest=null;
}
})
.exec(item.parentFragment.getAccountID());
}
}
private void updateOptionsMenu(){
if(item.parentFragment.getActivity()==null)
return;
Account account=item.user;
Relationship relationship=item.parentFragment.getRelationship(account.id);
Menu menu=optionsMenu.getMenu();
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
boolean canTranslate=item.status!=null && item.status.getContentStatus().isEligibleForTranslation();

View File

@ -204,7 +204,7 @@ public class HtmlParser{
Document doc=Jsoup.parseBodyFragment(html);
doc.body().select("span.invisible").remove();
Cleaner cleaner=new Cleaner(Safelist.none());
return cleaner.clean(doc).body().html();
return cleaner.clean(doc).body().text();
}
public static CharSequence parseLinks(String text){

View File

@ -835,4 +835,18 @@ public class UiUtils{
Toast.makeText(context, R.string.text_copied, Toast.LENGTH_SHORT).show();
}
}
public static void setAllPaddings(View view, int paddingDp){
int pad=V.dp(paddingDp);
view.setPadding(pad, pad, pad, pad);
}
public static ViewGroup.MarginLayoutParams makeLayoutParams(int width, int height, int marginStart, int marginTop, int marginEnd, int marginBottom){
ViewGroup.MarginLayoutParams lp=new ViewGroup.MarginLayoutParams(width>0 ? V.dp(width) : width, height>0 ? V.dp(height) : height);
lp.topMargin=V.dp(marginTop);
lp.bottomMargin=V.dp(marginBottom);
lp.setMarginStart(V.dp(marginStart));
lp.setMarginEnd(V.dp(marginEnd));
return lp;
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="12dp"/>
<solid android:color="?colorM3SurfaceVariant"/>
<stroke android:color="?colorM3OutlineVariant" android:width="1dp"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<stroke android:width="1dp" android:color="?colorM3OutlineVariant"/>
<corners android:radius="11dp"/>
</shape>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,23Q19.5,23 21.25,21.25Q23,19.5 23,17H21.5Q21.5,18.875 20.188,20.188Q18.875,21.5 17,21.5ZM1,7H2.5Q2.5,5.125 3.812,3.812Q5.125,2.5 7,2.5V1Q4.5,1 2.75,2.75Q1,4.5 1,7ZM3.525,20.475Q1.025,17.975 1.025,14.475Q1.025,10.975 3.525,8.475L7.05,4.925Q7.775,4.2 8.812,4.2Q9.85,4.2 10.575,4.925Q10.675,5.025 10.763,5.125Q10.85,5.225 10.9,5.325L13.425,2.8Q14.15,2.075 15.188,2.075Q16.225,2.075 16.95,2.8Q17.075,2.925 17.15,3.025Q17.225,3.125 17.3,3.225Q18.025,2.725 18.925,2.8Q19.825,2.875 20.475,3.525Q21.05,4.1 21.175,4.837Q21.3,5.575 21,6.3Q21.15,6.375 21.288,6.475Q21.425,6.575 21.55,6.7Q22.275,7.425 22.275,8.463Q22.275,9.5 21.55,10.225L21.15,10.625Q21.25,10.675 21.35,10.762Q21.45,10.85 21.55,10.95Q22.275,11.675 22.275,12.712Q22.275,13.75 21.55,14.475L15.525,20.475Q13.025,22.975 9.525,22.975Q6.025,22.975 3.525,20.475ZM4.925,19.075Q5.875,20.025 7.075,20.5Q8.275,20.975 9.525,20.975Q10.775,20.975 11.975,20.5Q13.175,20.025 14.125,19.075L20.125,13.05Q20.275,12.9 20.275,12.7Q20.275,12.5 20.125,12.35Q19.975,12.2 19.775,12.2Q19.575,12.2 19.425,12.35L15.9,15.9L14.475,14.475L20.125,8.825Q20.275,8.675 20.275,8.462Q20.275,8.25 20.125,8.1Q19.975,7.975 19.775,7.962Q19.575,7.95 19.425,8.1L14.475,13.05L13.05,11.65L19.075,5.625Q19.225,5.475 19.225,5.275Q19.225,5.075 19.075,4.925Q18.925,4.775 18.725,4.775Q18.525,4.775 18.375,4.925L12.35,10.95L10.95,9.525L15.525,4.925Q15.675,4.775 15.675,4.575Q15.675,4.375 15.525,4.225Q15.4,4.075 15.188,4.075Q14.975,4.075 14.825,4.225L8.525,10.525Q9.05,11.875 8.775,13.337Q8.5,14.8 7.4,15.9L6,14.475Q6.875,13.6 6.875,12.35Q6.875,11.1 6,10.225L9.175,7.05Q9.325,6.9 9.325,6.7Q9.325,6.5 9.175,6.35Q9.025,6.2 8.825,6.212Q8.625,6.225 8.475,6.35L4.925,9.875Q3.975,10.825 3.5,12.025Q3.025,13.225 3.025,14.475Q3.025,15.725 3.5,16.925Q3.975,18.125 4.925,19.075Z"/>
</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:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="8dp"
android:baselineAligned="false">
<TextView
android:id="@+id/number"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignParentTop="true"
android:layout_alignParentStart="true"
android:layout_marginEnd="16dp"
android:textColor="?colorM3Primary"
android:fontFamily="sans-serif-condensed"
android:textStyle="bold"
android:textSize="22dp"
android:gravity="center"
android:includeFontPadding="false"
tools:text="1"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/number"
android:textAppearance="@style/m3_body_large"
android:textSize="16sp"
android:textColor="?colorM3OnSurface"
android:minHeight="20sp"
android:gravity="center_vertical|start"
tools:text="No discrimination, including (but not limited to) racism, sexism, homophobia or transphobia."/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/number"
android:layout_below="@id/title"
android:textAppearance="@style/m3_body_medium"
android:textSize="14sp"
android:textColor="?colorM3OnSurfaceVariant"
android:paddingVertical="2sp"
android:lineSpacingExtra="4sp"
android:gravity="start"
tools:text="No discrimination, including (but not limited to) racism, sexism, homophobia or transphobia."/>
</RelativeLayout>

View File

@ -0,0 +1,82 @@
<?xml version="1.0" encoding="utf-8"?>
<org.joinmastodon.android.ui.views.CustomScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_bottom_sheet">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp">
<View
android:id="@+id/handle"
android:layout_width="match_parent"
android:layout_height="36dp"
android:background="@drawable/bg_bottom_sheet_handle"/>
<ImageView
android:id="@+id/icon"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_below="@id/handle"
android:layout_alignParentStart="true"
android:layout_marginEnd="16dp"
android:background="@drawable/white_circle"
android:backgroundTint="?colorM3PrimaryContainer"
android:scaleType="center"
android:tint="?colorM3OnPrimaryContainer"
tools:src="@drawable/ic_waving_hand_24px"
android:importantForAccessibility="no"/>
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/icon"
android:layout_below="@id/handle"
android:layout_marginBottom="4dp"
android:textAppearance="@style/m3_title_large"
android:fontFamily="sans-serif"
android:textColor="?colorM3OnSurface"
tools:text="@string/non_mutual_sheet_title"/>
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_toEndOf="@id/icon"
android:textAppearance="@style/m3_body_medium"
tools:text="@string/non_mutual_sheet_text"/>
<LinearLayout
android:id="@+id/content_wrap"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:minHeight="8dp"
android:orientation="vertical"/>
<Button
android:id="@+id/btn_got_it"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@id/content_wrap"
android:layout_marginBottom="8dp"
style="@style/Widget.Mastodon.M3.Button.Tonal"
android:text="@string/got_it"/>
<Button
android:id="@+id/btn_dont_remind_again"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_below="@id/btn_got_it"
android:layout_marginBottom="8dp"
style="@style/Widget.Mastodon.M3.Button.Text"
android:text="@string/dont_remind_again"/>
</RelativeLayout>
</org.joinmastodon.android.ui.views.CustomScrollView>

View File

@ -658,4 +658,22 @@
<string name="list_find_users">Find users to add</string>
<string name="reply_to_user">Reply to %s</string>
<string name="posted_at">Posted at %s</string>
<string name="non_mutual_sheet_title">Hello, new connection!</string>
<string name="non_mutual_sheet_text">Looks like youre about to reply to someone who isnt a mutual connection yet. Lets make a great first impression.</string>
<string name="got_it">Got it</string>
<string name="dont_remind_again">Dont remind me again</string>
<!-- %s is a time interval ("5 months") -->
<string name="old_post_sheet_title">This post is %s old</string>
<string name="old_post_sheet_text">You can still reply, but it may no longer be relevant.</string>
<plurals name="x_months">
<item quantity="one">%,d month</item>
<item quantity="other">%,d months</item>
</plurals>
<string name="more_than_two_years">more than 2 years</string>
<string name="non_mutual_title1">Stay respectful &amp; relevant</string>
<string name="non_mutual_text1">Ensure your reply is courteous and on-topic.</string>
<string name="non_mutual_title2">Embrace kindness</string>
<string name="non_mutual_text2">A positive tone is always appreciated.</string>
<string name="non_mutual_title3">Be open</string>
<string name="non_mutual_text3">Everyones conversation style is unique. Be ready to adapt.</string>
</resources>