diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java index 1a02d18f28..d1e8553ff8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetAccountStatuses.java @@ -21,6 +21,7 @@ public GetAccountStatuses(String id, String maxID, String minID, int limit, @Non switch(filter){ case DEFAULT -> addQueryParameter("exclude_replies", "true"); case INCLUDE_REPLIES -> {} + case PINNED -> addQueryParameter("pinned", "true"); case MEDIA -> addQueryParameter("only_media", "true"); case NO_REBLOGS -> { addQueryParameter("exclude_replies", "true"); @@ -33,6 +34,7 @@ public GetAccountStatuses(String id, String maxID, String minID, int limit, @Non public enum Filter{ DEFAULT, INCLUDE_REPLIES, + PINNED, MEDIA, NO_REBLOGS, OWN_POSTS_AND_REPLIES diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/SetStatusPinned.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/SetStatusPinned.java new file mode 100644 index 0000000000..e7c55e63fa --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/SetStatusPinned.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.api.requests.statuses; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Status; + +public class SetStatusPinned extends MastodonAPIRequest{ + public SetStatusPinned(String id, boolean pinned){ + super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class); + setRequestBody(new Object()); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java index 926f0bf67b..a786e9c70c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java +++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusCountersUpdatedEvent.java @@ -5,7 +5,7 @@ public class StatusCountersUpdatedEvent{ public String id; public long favorites, reblogs, replies; - public boolean favorited, reblogged; + public boolean favorited, reblogged, pinned; public StatusCountersUpdatedEvent(Status s){ id=s.id; @@ -14,5 +14,6 @@ public StatusCountersUpdatedEvent(Status s){ replies=s.repliesCount; favorited=s.favourited; reblogged=s.reblogged; + pinned=s.pinned; } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusUnpinnedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusUnpinnedEvent.java new file mode 100644 index 0000000000..54925c076a --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusUnpinnedEvent.java @@ -0,0 +1,11 @@ +package org.joinmastodon.android.events; + +public class StatusUnpinnedEvent { + public final String id; + public final String accountID; + + public StatusUnpinnedEvent(String id, String accountID){ + this.id=id; + this.accountID=accountID; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java index 7924e9c93d..ea9abbdd16 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java @@ -8,8 +8,10 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.StatusCreatedEvent; +import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; import org.parceler.Parcels; import java.util.Collections; @@ -76,6 +78,7 @@ protected void onShown(){ protected void onStatusCreated(StatusCreatedEvent ev){ if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account)) return; + if(filter==GetAccountStatuses.Filter.PINNED) return; if(filter==GetAccountStatuses.Filter.DEFAULT){ // Keep replies to self, discard all other replies if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id)) @@ -86,4 +89,24 @@ protected void onStatusCreated(StatusCreatedEvent ev){ } prependItems(Collections.singletonList(ev.status), true); } + + protected void onStatusUnpinned(StatusUnpinnedEvent ev){ + if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED) + return; + + Status status=getStatusByID(ev.id); + data.remove(status); + preloadedData.remove(status); + HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class); + if(item==null) + return; + int index=displayItems.indexOf(item); + int lastIndex; + for(lastIndex=index;lastIndex R.id.profile_posts; case 1 -> R.id.profile_posts_with_replies; - case 2 -> R.id.profile_media; - case 3 -> R.id.profile_about; + case 2 -> R.id.profile_pinned_posts; + case 3 -> R.id.profile_media; + case 4 -> R.id.profile_about; default -> throw new IllegalStateException("Unexpected value: "+i); }); tabView.setVisibility(View.GONE); @@ -224,7 +225,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ tabViews[i]=tabView; } - pager.setOffscreenPageLimit(4); + pager.setOffscreenPageLimit(5); pager.setAdapter(new ProfilePagerAdapter()); pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels; @@ -240,8 +241,9 @@ public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ tab.setText(switch(position){ case 0 -> R.string.posts; case 1 -> R.string.posts_and_replies; - case 2 -> R.string.media; - case 3 -> R.string.profile_about; + case 2 -> R.string.pinned_posts; + case 3 -> R.string.media; + case 4 -> R.string.profile_about; default -> throw new IllegalStateException(); }); } @@ -298,6 +300,8 @@ public void onSuccess(Account result){ postsFragment.onRefresh(); if(postsWithRepliesFragment.loaded) postsWithRepliesFragment.onRefresh(); + if(pinnedPostsFragment.loaded) + pinnedPostsFragment.onRefresh(); if(mediaFragment.loaded) mediaFragment.onRefresh(); } @@ -322,6 +326,7 @@ public void dataLoaded(){ if(postsFragment==null){ postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true); postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false); + pinnedPostsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false); mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false); aboutFragment=new ProfileAboutFragment(); aboutFragment.setFields(fields); @@ -402,6 +407,7 @@ private void applyChildWindowInsets(){ if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){ postsFragment.onApplyWindowInsets(childInsets); postsWithRepliesFragment.onApplyWindowInsets(childInsets); + pinnedPostsFragment.onApplyWindowInsets(childInsets); mediaFragment.onApplyWindowInsets(childInsets); } } @@ -652,8 +658,9 @@ private Fragment getFragmentForPage(int page){ return switch(page){ case 0 -> postsFragment; case 1 -> postsWithRepliesFragment; - case 2 -> mediaFragment; - case 3 -> aboutFragment; + case 2 -> pinnedPostsFragment; + case 3 -> mediaFragment; + case 4 -> aboutFragment; default -> throw new IllegalStateException(); }; } @@ -714,9 +721,9 @@ private void enterEditMode(Account account){ invalidateOptionsMenu(); pager.setUserInputEnabled(false); actionButton.setText(R.string.done); - pager.setCurrentItem(3); + pager.setCurrentItem(4); ArrayList animators=new ArrayList<>(); - for(int i=0;i<3;i++){ + for(int i=0;i animators=new ArrayList<>(); actionButton.setText(R.string.edit_profile); - for(int i=0;i<3;i++){ + for(int i=0;i{}); + }else if(id==R.id.pin || id==R.id.unpin){ + UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{}); }else if(id==R.id.mute){ UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{}); }else if(id==R.id.block){ @@ -280,6 +282,8 @@ private void updateOptionsMenu(){ boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account); menu.findItem(R.id.edit).setVisible(item.status!=null && isOwnPost); menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost); + menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned); + menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned); menu.findItem(R.id.open_in_browser).setVisible(item.status!=null); MenuItem blockDomain=menu.findItem(R.id.block_domain); MenuItem mute=menu.findItem(R.id.mute); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 84436cca2e..6192cf657c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -11,7 +11,6 @@ import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; @@ -42,8 +41,11 @@ import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked; import org.joinmastodon.android.api.requests.statuses.DeleteStatus; import org.joinmastodon.android.api.requests.statuses.GetStatusByID; +import org.joinmastodon.android.api.requests.statuses.SetStatusPinned; import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.events.StatusCountersUpdatedEvent; import org.joinmastodon.android.events.StatusDeletedEvent; +import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.fragments.HashtagTimelineFragment; import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ThreadFragment; @@ -400,6 +402,32 @@ public void onError(ErrorResponse error){ }); } + public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer resultCallback){ + showConfirmationAlert(activity, + pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title, + pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post, + pinned ? R.string.pin_post : R.string.unpin_post, + ()->{ + new SetStatusPinned(status.id, pinned) + .setCallback(new Callback<>() { + @Override + public void onSuccess(Status result) { + resultCallback.accept(result); + E.post(new StatusCountersUpdatedEvent(result)); + if (!result.pinned) + E.post(new StatusUnpinnedEvent(status.id, accountID)); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(activity); + } + }) + .wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false) + .exec(accountID); + }); + } + public static void setRelationshipToActionButton(Relationship relationship, Button button){ boolean secondaryStyle; if(relationship.blocking){ diff --git a/mastodon/src/main/res/menu/post.xml b/mastodon/src/main/res/menu/post.xml index ca14e2b700..e926442071 100644 --- a/mastodon/src/main/res/menu/post.xml +++ b/mastodon/src/main/res/menu/post.xml @@ -2,6 +2,8 @@ + + diff --git a/mastodon/src/main/res/values-de-rDE/strings.xml b/mastodon/src/main/res/values-de-rDE/strings.xml index 0c19347fba..64c3641757 100644 --- a/mastodon/src/main/res/values-de-rDE/strings.xml +++ b/mastodon/src/main/res/values-de-rDE/strings.xml @@ -41,6 +41,7 @@ Beiträge Beiträge & Antworten + Angeheftet Medien Über Folgen @@ -123,6 +124,14 @@ Beitrag löschen Bist du dir sicher, dass du den Beitrag löschen möchtest? wird gelöscht … + An Profil anheften + Beitrag an Profil anheften + Möchtest du den Beitrag an dein Profil anheften? + Wird angeheftet… + Von Profil lösen + Angehefteten Beitrag von Profil lösen + Bist du dir sicher, dass du den angehefteten Beitrag von deinem Profil lösen möchtest? + Wird vom Profil gelöst… Audiowiedergabe Abspielen Pausieren diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml index 6545f9cad7..87c3103055 100644 --- a/mastodon/src/main/res/values/ids.xml +++ b/mastodon/src/main/res/values/ids.xml @@ -4,6 +4,7 @@ + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 2ee4d99753..7638e2fe90 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -47,6 +47,7 @@ Posts Posts and Replies + Pinned Media About Follow @@ -129,6 +130,14 @@ Delete Post Are you sure you want to delete this post? Deleting… + Pin to profile + Pin post to profile + Do you want to pin this post to your profile? + Pinning post… + Unpin from profile + Unpin post from profile + Are you sure you want to unpin this post? + Unpinning post… Audio playback Play Pause