diff --git a/mastodon/build.gradle b/mastodon/build.gradle index 16668ac6..f618e1d0 100644 --- a/mastodon/build.gradle +++ b/mastodon/build.gradle @@ -10,7 +10,7 @@ android { applicationId "org.joinmastodon.android" minSdk 23 targetSdk 31 - versionCode 5 + versionCode 6 versionName "0.1" } diff --git a/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java index 60ca2f80..ab0c29d1 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java +++ b/mastodon/src/main/java/org/joinmastodon/android/AudioPlayerService.java @@ -37,6 +37,7 @@ import java.util.HashSet; import androidx.annotation.Nullable; import me.grishka.appkit.imageloader.ImageCache; import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.V; public class AudioPlayerService extends Service{ private static final int NOTIFICATION_SERVICE=1; @@ -153,10 +154,10 @@ public class AudioPlayerService extends Service{ } }); - Drawable d=ImageCache.getInstance(this).getFromTop(new UrlImageLoaderRequest(status.account.avatar)); + Drawable d=ImageCache.getInstance(this).getFromTop(new UrlImageLoaderRequest(status.account.avatar, V.dp(50), V.dp(50))); if(d instanceof BitmapDrawable){ statusAvatar=((BitmapDrawable) d).getBitmap(); - }else{ + }else if(d!=null){ statusAvatar=Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight()); d.draw(new Canvas(statusAvatar)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountRelationships.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountRelationships.java index fec22bbe..9c62e58e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountRelationships.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountRelationships.java @@ -5,12 +5,13 @@ import com.google.gson.reflect.TypeToken; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Relationship; +import java.util.Collection; import java.util.List; import androidx.annotation.NonNull; public class GetAccountRelationships extends MastodonAPIRequest>{ - public GetAccountRelationships(@NonNull List ids){ + public GetAccountRelationships(@NonNull Collection ids){ super(HttpMethod.GET, "/accounts/relationships", new TypeToken<>(){}); for(String id:ids) addQueryParameter("id[]", id); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java index c5dc0a72..9cf925cb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/notifications/GetNotifications.java @@ -1,18 +1,27 @@ package org.joinmastodon.android.api.requests.notifications; +import com.google.gson.annotations.SerializedName; import com.google.gson.reflect.TypeToken; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Notification; +import java.util.EnumSet; import java.util.List; public class GetNotifications extends MastodonAPIRequest>{ - public GetNotifications(String maxID, int limit){ + public GetNotifications(String maxID, int limit, EnumSet excludeTypes){ super(HttpMethod.GET, "/notifications", new TypeToken<>(){}); if(maxID!=null) addQueryParameter("max_id", maxID); if(limit>0) addQueryParameter("limit", ""+limit); + if(excludeTypes!=null){ + for(Notification.Type nt:excludeTypes){ + try{ + addQueryParameter("exclude_types[]", nt.getDeclaringClass().getField(nt.name()).getAnnotation(SerializedName.class).value()); + }catch(NoSuchFieldException ignore){} + } + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java index 3827d10a..6358c2e0 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java @@ -16,10 +16,12 @@ import android.view.ViewTreeObserver; import android.widget.Toolbar; 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.model.Account; import org.joinmastodon.android.model.DisplayItemsParent; import org.joinmastodon.android.model.Poll; +import org.joinmastodon.android.model.Relationship; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.BetterItemAnimator; import org.joinmastodon.android.ui.PhotoLayoutHelper; @@ -41,6 +43,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; import androidx.annotation.NonNull; @@ -63,6 +66,7 @@ public abstract class BaseStatusListFragment exten protected String accountID; protected PhotoViewer currentPhotoViewer; protected HashMap knownAccounts=new HashMap<>(); + protected HashMap relationships=new HashMap<>(); public BaseStatusListFragment(){ super(20); @@ -255,6 +259,7 @@ public abstract class BaseStatusListFragment exten } }); list.addItemDecoration(new RecyclerView.ItemDecoration(){ + private Rect tmpRect=new Rect(); private Paint paint=new Paint(); { paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted)); @@ -264,13 +269,18 @@ public abstract class BaseStatusListFragment exten @Override public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ - for(int i=0;i) holder).getItemID().equals(((StatusDisplayItem.Holder) siblingHolder).getItemID())){ + parent.getDecoratedBoundsWithMargins(child, tmpRect); + tmpRect.offset(0, Math.round(child.getTranslationY())); + float y=tmpRect.bottom-V.dp(.5f); paint.setAlpha(Math.round(255*child.getAlpha())); - c.drawLine(child.getX(), y, child.getX()+child.getWidth(), y, paint); + c.drawLine(0, y, parent.getWidth(), y, paint); } } } @@ -316,9 +326,10 @@ public abstract class BaseStatusListFragment exten } }); ((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){ + private Rect tmpRect=new Rect(); @Override public void getSelectorBounds(View view, Rect outRect){ - outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()); + list.getDecoratedBoundsWithMargins(view, outRect); RecyclerView.ViewHolder holder=list.getChildViewHolder(view); if(holder instanceof StatusDisplayItem.Holder){ String id=((StatusDisplayItem.Holder) holder).getItemID(); @@ -328,10 +339,11 @@ public abstract class BaseStatusListFragment exten if(holder instanceof StatusDisplayItem.Holder){ String otherID=((StatusDisplayItem.Holder) holder).getItemID(); if(otherID.equals(id)){ - outRect.left=Math.min(outRect.left, child.getLeft()); - outRect.top=Math.min(outRect.top, child.getTop()); - outRect.right=Math.max(outRect.right, child.getRight()); - outRect.bottom=Math.max(outRect.bottom, child.getBottom()); + list.getDecoratedBoundsWithMargins(child, tmpRect); + outRect.left=Math.min(outRect.left, tmpRect.left); + outRect.top=Math.min(outRect.top, tmpRect.top); + outRect.right=Math.max(outRect.right, tmpRect.right); + outRect.bottom=Math.max(outRect.bottom, tmpRect.bottom); } } } @@ -509,6 +521,37 @@ public abstract class BaseStatusListFragment exten return accountID; } + public Relationship getRelationship(String id){ + return relationships.get(id); + } + + public void putRelationship(String id, Relationship rel){ + relationships.put(id, rel); + } + + protected void loadRelationships(Set ids){ + if(ids.isEmpty()) + return; + // TODO somehow manage these and cancel outstanding requests on refresh + new GetAccountRelationships(ids) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + for(Relationship r:result) + relationships.put(r.id, r); + onRelationshipsLoaded(); + } + + @Override + public void onError(ErrorResponse error){ + + } + }) + .exec(accountID); + } + + protected void onRelationshipsLoaded(){} + @Nullable protected I findItemOfType(String id, Class type){ for(StatusDisplayItem item:displayItems){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 4ab18352..4dcc8345 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -170,6 +170,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene lf.loadData(); }else if(newFragment instanceof DiscoverFragment){ ((DiscoverFragment) newFragment).loadData(); + }else if(newFragment instanceof NotificationsFragment){ + ((NotificationsFragment) newFragment).loadData(); + // TODO make an interface? } currentTab=tab; ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java index efe6d37a..1c84276c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java @@ -1,25 +1,49 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.app.Fragment; +import android.content.res.Configuration; +import android.os.Build; import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.notifications.GetNotifications; -import org.joinmastodon.android.model.Notification; -import org.joinmastodon.android.model.Poll; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; -import org.parceler.Parcels; +import org.joinmastodon.android.ui.SimpleViewHolder; +import org.joinmastodon.android.ui.tabs.TabLayout; +import org.joinmastodon.android.ui.tabs.TabLayoutMediator; +import org.joinmastodon.android.ui.utils.UiUtils; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; +import me.grishka.appkit.fragments.BaseRecyclerFragment; +import me.grishka.appkit.fragments.ToolbarFragment; +import me.grishka.appkit.utils.V; -import me.grishka.appkit.Nav; -import me.grishka.appkit.api.SimpleCallback; +public class NotificationsFragment extends ToolbarFragment implements ScrollableToTop{ + + private TabLayout tabLayout; + private ViewPager2 pager; + private FrameLayout[] tabViews; + private TabLayoutMediator tabLayoutMediator; + + private NotificationsListFragment allNotificationsFragment, mentionsFragment; + + private String accountID; + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) + setRetainInstance(true); + + accountID=getArguments().getString("account"); + } -public class NotificationsFragment extends BaseStatusListFragment{ @Override public void onAttach(Activity activity){ super.onAttach(activity); @@ -27,80 +51,135 @@ public class NotificationsFragment extends BaseStatusListFragment{ } @Override - protected List buildDisplayItems(Notification n){ - ReblogOrReplyLineStatusDisplayItem titleItem=new ReblogOrReplyLineStatusDisplayItem(n.id, this, switch(n.type){ - case FOLLOW -> getString(R.string.user_followed_you, n.account.displayName); - case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request, n.account.displayName); - case MENTION -> getString(R.string.user_mentioned_you, n.account.displayName); - case REBLOG -> getString(R.string.user_boosted, n.account.displayName); - case FAVORITE -> getString(R.string.user_favorited, n.account.displayName); - case POLL -> getString(R.string.poll_ended); - case STATUS -> getString(R.string.user_posted, n.account.displayName); - }, n.account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled); - if(n.status!=null){ - ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts); - items.add(0, titleItem); - return items; - }else{ - return Collections.singletonList(titleItem); + public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ + LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false); + + tabLayout=view.findViewById(R.id.tabbar); + pager=view.findViewById(R.id.pager); + + tabViews=new FrameLayout[2]; + for(int i=0;i R.id.notifications_all; + case 1 -> R.id.notifications_mentions; + default -> throw new IllegalStateException("Unexpected value: "+i); + }); + tabView.setVisibility(View.GONE); + view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment + tabViews[i]=tabView; } - } - @Override - protected void addAccountToKnown(Notification s){ - if(!knownAccounts.containsKey(s.account.id)) - knownAccounts.put(s.account.id, s.account); - if(s.status!=null && !knownAccounts.containsKey(s.status.account.id)) - knownAccounts.put(s.status.account.id, s.status.account); - } + tabLayout.setTabTextSize(V.dp(16)); + tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); - @Override - protected void doLoadData(int offset, int count){ - new GetNotifications(offset>0 ? getMaxID() : null, count) - .setCallback(new SimpleCallback<>(this){ - @Override - public void onSuccess(List result){ - onDataLoaded(result, !result.isEmpty()); - } - }) - .exec(accountID); - } + pager.setOffscreenPageLimit(4); + pager.setAdapter(new DiscoverPagerAdapter()); + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ + @Override + public void onPageSelected(int position){ + if(position==0) + return; + Fragment _page=getFragmentForPage(position); + if(_page instanceof BaseRecyclerFragment){ + BaseRecyclerFragment page=(BaseRecyclerFragment) _page; + if(!page.loaded && !page.isDataLoading()) + page.loadData(); + } + } + }); - @Override - protected void onShown(){ - super.onShown(); - if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) - loadData(); - } - - @Override - public void onItemClick(String id){ - Notification n=getNotificationByID(id); - if(n.status!=null){ - Status status=n.status; + if(allNotificationsFragment==null){ Bundle args=new Bundle(); args.putString("account", accountID); - args.putParcelable("status", Parcels.wrap(status)); - if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)) - args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId))); - Nav.go(getActivity(), ThreadFragment.class, args); + args.putBoolean("__is_tab", true); + + allNotificationsFragment=new NotificationsListFragment(); + allNotificationsFragment.setArguments(args); + + args=new Bundle(args); + args.putBoolean("onlyMentions", true); + mentionsFragment=new NotificationsListFragment(); + mentionsFragment.setArguments(args); + + getChildFragmentManager().beginTransaction() + .add(R.id.notifications_all, allNotificationsFragment) + .add(R.id.notifications_mentions, mentionsFragment) + .commit(); } + + tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){ + @Override + public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ + tab.setText(switch(position){ + case 0 -> R.string.all_notifications; + case 1 -> R.string.mentions; + default -> throw new IllegalStateException("Unexpected value: "+position); + }); + tab.view.textView.setAllCaps(true); + } + }); + tabLayoutMediator.attach(); + + return view; } @Override - protected void updatePoll(String itemID, Poll poll){ - Notification notification=getNotificationByID(itemID); - if(notification==null || notification.status==null) - return; - notification.status.poll=poll; - super.updatePoll(itemID, poll); + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + updateToolbar(); } - private Notification getNotificationByID(String id){ - for(Notification n:data){ - if(n.id.equals(id)) - return n; + @Override + public void onConfigurationChanged(Configuration newConfig){ + super.onConfigurationChanged(newConfig); + updateToolbar(); + } + + @Override + public void scrollToTop(){ + getFragmentForPage(pager.getCurrentItem()).scrollToTop(); + } + + public void loadData(){ + if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading) + allNotificationsFragment.loadData(); + } + + private void updateToolbar(){ + getToolbar().setOutlineProvider(null); + } + + private NotificationsListFragment getFragmentForPage(int page){ + return switch(page){ + case 0 -> allNotificationsFragment; + case 1 -> mentionsFragment; + default -> throw new IllegalStateException("Unexpected value: "+page); + }; + } + + private class DiscoverPagerAdapter extends RecyclerView.Adapter{ + @NonNull + @Override + public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + FrameLayout view=tabViews[viewType]; + ((ViewGroup)view.getParent()).removeView(view); + view.setVisibility(View.VISIBLE); + view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + return new SimpleViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){} + + @Override + public int getItemCount(){ + return 2; + } + + @Override + public int getItemViewType(int position){ + return position; } - return null; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java new file mode 100644 index 00000000..ad798c29 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -0,0 +1,222 @@ +package org.joinmastodon.android.fragments; + +import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Bundle; +import android.view.View; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.notifications.GetNotifications; +import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.Poll; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.ImageStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.parceler.Parcels; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.SimpleCallback; +import me.grishka.appkit.utils.V; + +public class NotificationsListFragment extends BaseStatusListFragment{ + private EnumSet types; + + @Override + public void onAttach(Activity activity){ + super.onAttach(activity); + setTitle(R.string.notifications); + if(getArguments().getBoolean("onlyMentions", false)){ + types=EnumSet.complementOf(EnumSet.of(Notification.Type.MENTION)); + } + } + + @Override + protected List buildDisplayItems(Notification n){ + String extraText=switch(n.type){ + case FOLLOW -> getString(R.string.user_followed_you); + case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request); + case MENTION, STATUS -> null; + case REBLOG -> getString(R.string.user_boosted); + case FAVORITE -> getString(R.string.user_favorited); + case POLL -> getString(R.string.poll_ended); + }; + HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null; + if(n.status!=null){ + ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null); + if(titleItem!=null) + items.add(0, titleItem); + return items; + }else{ + AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account); + return Arrays.asList(titleItem, card); + } + } + + @Override + protected void addAccountToKnown(Notification s){ + if(!knownAccounts.containsKey(s.account.id)) + knownAccounts.put(s.account.id, s.account); + if(s.status!=null && !knownAccounts.containsKey(s.status.account.id)) + knownAccounts.put(s.status.account.id, s.status.account); + } + + @Override + protected void doLoadData(int offset, int count){ + new GetNotifications(offset>0 ? getMaxID() : null, count, types) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + if(refreshing) + relationships.clear(); + onDataLoaded(result, !result.isEmpty()); + Set needRelationships=result.stream() + .filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id)) + .map(ntf->ntf.account.id) + .collect(Collectors.toSet()); + loadRelationships(needRelationships); + } + }) + .exec(accountID); + } + + @Override + protected void onRelationshipsLoaded(){ + if(getActivity()==null) + return; + for(int i=0;i) holder).getItem().inset; + if(inset){ + if(rect.isEmpty()){ + rect.set(child.getX(), i==0 && pos>0 && displayItems.get(pos-1).inset ? V.dp(-10) : child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight()); + }else{ + rect.bottom=Math.max(rect.bottom, child.getY()+child.getHeight()); + rect.right=Math.max(rect.right, child.getX()+child.getHeight()); + } + }else if(!rect.isEmpty()){ + drawInsetBackground(c); + rect.setEmpty(); + } + } + if(!rect.isEmpty()){ + if(pos) holder).getItem().inset; + int pos=holder.getAbsoluteAdapterPosition(); + if(inset){ + boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; + boolean bottomSiblingInset=pos{ protected List buildDisplayItems(Status s){ - return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts); + return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, true); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountCardStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountCardStatusDisplayItem.java new file mode 100644 index 00000000..196aa51a --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountCardStatusDisplayItem.java @@ -0,0 +1,166 @@ +package org.joinmastodon.android.ui.displayitems; + +import android.app.Activity; +import android.content.Context; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Relationship; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.ProgressBarButton; + +import java.util.Collections; + +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.V; + +public class AccountCardStatusDisplayItem extends StatusDisplayItem{ + private final Account account; + public ImageLoaderRequest avaRequest, coverRequest; + public CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + public CharSequence parsedName, parsedBio; + + public AccountCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){ + super(parentID, parentFragment); + this.account=account; + if(!TextUtils.isEmpty(account.avatar)) + avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50)); + if(!TextUtils.isEmpty(account.header)) + coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); + parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID()); + if(account.emojis.isEmpty()){ + parsedName=account.displayName; + }else{ + parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); + emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); + } + } + + @Override + public Type getType(){ + return Type.ACCOUNT_CARD; + } + + @Override + public int getImageCount(){ + return 2+emojiHelper.getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + return switch(index){ + case 0 -> avaRequest; + case 1 -> coverRequest; + default -> emojiHelper.getImageRequest(index-2); + }; + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final ImageView cover, avatar; + private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel; + private final ProgressBarButton actionButton; + private final ProgressBar actionProgress; + private final View actionWrap; + + private Relationship relationship; + + public Holder(Context context, ViewGroup parent){ + super(context, R.layout.display_item_account_card, parent); + + cover=findViewById(R.id.cover); + avatar=findViewById(R.id.avatar); + name=findViewById(R.id.name); + username=findViewById(R.id.username); + bio=findViewById(R.id.bio); + followersCount=findViewById(R.id.followers_count); + followersLabel=findViewById(R.id.followers_label); + followingCount=findViewById(R.id.following_count); + followingLabel=findViewById(R.id.following_label); + postsCount=findViewById(R.id.posts_count); + postsLabel=findViewById(R.id.posts_label); + actionButton=findViewById(R.id.action_btn); + actionProgress=findViewById(R.id.action_progress); + actionWrap=findViewById(R.id.action_btn_wrap); + + View card=findViewById(R.id.card); + card.setOutlineProvider(OutlineProviders.roundedRect(6)); + card.setClipToOutline(true); + avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); + avatar.setClipToOutline(true); + cover.setOutlineProvider(OutlineProviders.roundedRect(3)); + cover.setClipToOutline(true); + actionButton.setOnClickListener(this::onActionButtonClick); + } + + @Override + public void onBind(AccountCardStatusDisplayItem item){ + name.setText(item.parsedName); + username.setText('@'+item.account.acct); + bio.setText(item.parsedBio); + followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount)); + followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount)); + postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); + followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, Math.min(999, item.account.followersCount))); + followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, Math.min(999, item.account.followingCount))); + postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, Math.min(999, item.account.statusesCount))); + relationship=item.parentFragment.getRelationship(item.account.id); + if(relationship==null){ + actionWrap.setVisibility(View.GONE); + }else{ + actionWrap.setVisibility(View.VISIBLE); + UiUtils.setRelationshipToActionButton(relationship, actionButton); + } + } + + + private void onActionButtonClick(View v){ + itemView.setHasTransientState(true); + UiUtils.performAccountAction((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), relationship, actionButton, this::setActionProgressVisible, rel->{ + itemView.setHasTransientState(false); + item.parentFragment.putRelationship(item.account.id, rel); + rebind(); + }); + } + + private void setActionProgressVisible(boolean visible){ + actionButton.setTextVisible(!visible); + actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE); + actionButton.setClickable(!visible); + } + + @Override + public void setImage(int index, Drawable image){ + if(index==0){ + avatar.setImageDrawable(image); + }else if(index==1){ + cover.setImageDrawable(image); + }else{ + item.emojiHelper.setImageDrawable(index-2, image); + name.invalidate(); + bio.invalidate(); + } + if(image instanceof Animatable && !((Animatable) image).isRunning()) + ((Animatable) image).start(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index c83f28fa..c57e476e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -45,8 +45,9 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ public final Status status; private boolean hasVisibilityToggle; boolean needBottomPadding; + private String extraText; - public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status){ + public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, BaseStatusListFragment parentFragment, String accountID, Status status, String extraText){ super(parentID, parentFragment); this.user=user; this.createdAt=createdAt; @@ -56,15 +57,18 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ this.status=status; HtmlParser.parseCustomEmoji(parsedName, user.emojis); emojiHelper.setText(parsedName); - hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText); - if(!hasVisibilityToggle && !status.mediaAttachments.isEmpty()){ - for(Attachment att:status.mediaAttachments){ - if(att.type!=Attachment.Type.AUDIO){ - hasVisibilityToggle=true; - break; + if(status!=null){ + hasVisibilityToggle=status.sensitive || !TextUtils.isEmpty(status.spoilerText); + if(!hasVisibilityToggle && !status.mediaAttachments.isEmpty()){ + for(Attachment att:status.mediaAttachments){ + if(att.type!=Attachment.Type.AUDIO){ + hasVisibilityToggle=true; + break; + } } } } + this.extraText=extraText; } @Override @@ -86,7 +90,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ } public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - private final TextView name, username, timestamp; + private final TextView name, username, timestamp, extraText; private final ImageView avatar, more, visibility; private static final ViewOutlineProvider roundCornersOutline=new ViewOutlineProvider(){ @@ -104,6 +108,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ avatar=findViewById(R.id.avatar); more=findViewById(R.id.more); visibility=findViewById(R.id.visibility); + extraText=findViewById(R.id.extra_text); avatar.setOnClickListener(this::onAvaClick); avatar.setOutlineProvider(roundCornersOutline); avatar.setClipToOutline(true); @@ -121,6 +126,13 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ visibility.setImageResource(item.status.spoilerRevealed ? R.drawable.ic_visibility_off : R.drawable.ic_visibility); } itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), item.needBottomPadding ? V.dp(16) : 0); + if(TextUtils.isEmpty(item.extraText)){ + extraText.setVisibility(View.GONE); + }else{ + extraText.setVisibility(View.VISIBLE); + extraText.setText(item.extraText); + } + more.setVisibility(item.inset ? View.GONE : View.VISIBLE); } @Override @@ -148,11 +160,11 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{ } private void onMoreClick(View v){ - Account account=item.status.account; + Account account=item.user; PopupMenu popup=new PopupMenu(v.getContext(), v); Menu menu=popup.getMenu(); popup.getMenuInflater().inflate(R.menu.post, menu); - if(!AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account)) + if(item.status==null || !AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account)) menu.findItem(R.id.delete).setVisible(false); menu.findItem(R.id.mute).setTitle(v.getResources().getString(/*relationship.muting ? R.string.unmute_user :*/ R.string.mute_user, account.displayName)); menu.findItem(R.id.block).setTitle(v.getResources().getString(/*relationship.blocking ? R.string.unblock_user :*/ R.string.block_user, account.displayName)); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 33cbd031..0b3b70e9 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -29,6 +29,7 @@ import me.grishka.appkit.views.UsableRecyclerView; public abstract class StatusDisplayItem{ public final String parentID; public final BaseStatusListFragment parentFragment; + public boolean inset; public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){ this.parentID=parentID; @@ -58,10 +59,11 @@ public abstract class StatusDisplayItem{ case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); + case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent); }; } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts){ + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter){ String parentID=parentObject.getID(); ArrayList items=new ArrayList<>(); Status statusForContent=status.getContentStatus(); @@ -72,7 +74,7 @@ public abstract class StatusDisplayItem{ items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_fluent_arrow_reply_20_filled)); } HeaderStatusDisplayItem header; - items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent)); + items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); if(!TextUtils.isEmpty(statusForContent.content)) items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent)); else @@ -102,10 +104,14 @@ public abstract class StatusDisplayItem{ if(statusForContent.poll!=null){ buildPollItems(parentID, fragment, statusForContent.poll, items); } - if(statusForContent.card!=null){ + if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){ items.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent)); } - items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID)); + if(addFooter){ + items.add(new FooterStatusDisplayItem(parentID, fragment, statusForContent, accountID)); + } + for(StatusDisplayItem item:items) + item.inset=inset; return items; } @@ -128,6 +134,7 @@ public abstract class StatusDisplayItem{ POLL_FOOTER, CARD, FOOTER, + ACCOUNT_CARD, } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.Clickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HeaderSubtitleLinearLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HeaderSubtitleLinearLayout.java index 5470dce6..95446428 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/HeaderSubtitleLinearLayout.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/HeaderSubtitleLinearLayout.java @@ -24,10 +24,12 @@ public class HeaderSubtitleLinearLayout extends LinearLayout{ @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ - if(getChildCount()>1){ + if(getLayoutChildCount()>1){ int remainingWidth=MeasureSpec.getSize(widthMeasureSpec); for(int i=1;i + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_header.xml b/mastodon/src/main/res/layout/display_item_header.xml index 69f3ca56..ee384165 100644 --- a/mastodon/src/main/res/layout/display_item_header.xml +++ b/mastodon/src/main/res/layout/display_item_header.xml @@ -37,22 +37,42 @@ android:layout_alignParentTop="true" android:layout_marginEnd="12dp" /> - + android:layout_marginEnd="8dp"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml index ff44c719..094823ed 100644 --- a/mastodon/src/main/res/values/ids.xml +++ b/mastodon/src/main/res/values/ids.xml @@ -11,4 +11,7 @@ + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index a1cb04b1..ca14d699 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -14,12 +14,10 @@ In reply to %s Notifications - %s followed you - %s sent you a follow request - %s mentioned you - %s favorited your toot - Poll you voted in has ended - %s posted + followed you + sent you a follow request + favorited your toot + poll ended %ds %dm @@ -137,6 +135,8 @@ Hashtags News For you + All + Mentions %d person is talking %d people are talking