Merge upstream redesign (#714)

* merge toolbar fragment

* Fix store screenshot generator

* Fix alert color

* Fix #609

* Fix crash

* bigger hitbox for chips

* support mastodon languages

* merge ui utils

* merge stuff

* fix icon

* ensure 48dp touch target

* init local prefs, add helper function for enum values

* update compose action layout

* merge compose-adj files

* update extended footer

* fix poll wrong option checked

closes sk22#641

* no border when disabled

closes sk22#640

* Fix #610

* Minor fixes

* Fix alert color

* Fix #609

* Fix crash

* Fix #610

* Minor fixes

* add resources

* more compatible mastodon language

* fix html parser

* mark as read on refresh

* update tab bar

* tweak m3 buttons

* update compose-adj files

* tweak and update styles

* m3 expand button

* flag icon should be 18dp, actually

* More minor fixes

closes #612

* More minor fixes

closes #612

* Bump version

* fix no create status event when redrafting

* add material 3 assets

* New translations strings.xml (Greek)

* New translations strings.xml (Greek)

* New translations strings.xml (Italian)

* New translations strings.xml (Greek)

* New translations strings.xml (Italian)

* New translations strings.xml (Thai)

* New translations strings.xml (Thai)

* New translations strings.xml (Italian)

* New translations strings.xml (Thai)

* use new buttons for profile fragment

* merge compose fragment

* merge all the styles! oh dear

* New translations full_description.txt (Indonesian)

* New translations full_description.txt (Chinese Simplified)

* New translations strings.xml (Chinese Simplified)

* New translations full_description.txt (Chinese Simplified)

* Fix #615

* Minor fixes

* Fix #611

* A bunch of crash fixes

* New translations strings.xml (Greek)

* Make the default server configurable

* Pass the system timezone to server when signing up

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Japanese)

* Fix #615

* Minor fixes

* Fix #611

* A bunch of crash fixes

* Make the default server configurable

* Pass the system timezone to server when signing up

* oops. accidentally pasted the commit message in the code

* Remove unused code that caused a crash for some users ¯\_(ツ)_/¯

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* Remove unused code that caused a crash for some users ¯\_(ツ)_/¯

* New translations strings.xml (Polish)

* New translations strings.xml (Polish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Belarusian)

* prepare merging profile fragment

* merge profile fragment

* New translations strings.xml (Belarusian)

* New translations strings.xml (Greek)

* fix icon padding

* apply post header changes

* minor margin tweaks

* fix footer buttons

* fix header announcement buttons

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations strings.xml (Japanese)

* New translations full_description.txt (Japanese)

* New translations strings.xml (Icelandic)

* New translations strings.xml (Icelandic)

* New translations strings.xml (Icelandic)

* fix replying

* New translations strings.xml (Icelandic)

* fix translate button

* fix more button visibility

* fix counts label styling

* fix disabled boost button opacity

* fix tab layouts

* fix notification icon color crash

* New translations strings.xml (Greek)

* implement elevation listener in home tab

* fix elevation and listener in home tab

* add elevation scroll listener to notifications

* New translations strings.xml (Scottish Gaelic)

* Add editorconfig

So that PRs like #625 don't happen again

* Crash fix

* 🤔

* New translations strings.xml (Greek)

* New translations strings.xml (Japanese)

* New translations strings.xml (French)

* New translations strings.xml (French)

* New translations strings.xml (French)

* fix notification elevation and integrate divider

* 🤔

* Crash fix

* Add editorconfig

So that PRs like #625 don't happen again

* New translations strings.xml (Turkish)

* save interactions in cache

* New translations strings.xml (Turkish)

* merge new discover/search

* New translations strings.xml (Bengali)

* New translations strings.xml (Scottish Gaelic)

* New translations strings.xml (Bengali)

* merge new settings fragments

* fix no auth callback always being executed

* allow opening server info from profile

closes sk22#593

* fix hide boosts icon color

closes sk22#676

* New translations strings.xml (Turkish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (Turkish)

* New translations strings.xml (Chinese Simplified)

* New translations strings.xml (German)

* New translations strings.xml (German)

* New translations strings.xml (Turkish)

* update fedinuke list

from source; doesn't contain any modifications regarding a recent issue

* New translations strings.xml (Turkish)

* remove unused class

* fix crash

* darken m3 outline color a bit

* use m3 outline again

* fix misalignment

closes sk22#682

* New translations strings.xml (Turkish)

* New translations full_description.txt (Turkish)

* New translations short_description.txt (Turkish)

* fix crash

* fix metadata sorting

* show pronouns in header/account lists

* fix broken divider line

closes sk22#679

* trim pronouns

* improve pronoun display

* New translations strings.xml (French)

* New translations strings.xml (Japanese)

* fix broken federated timeline

closes sk22#685

* fix broken -1 fallback behavior

closes sk22#681

* don't display nothing if server about request fails

closes sk22#678

* New translations strings.xml (Ukrainian)

* migrate global prefs to local prefs

* do confirm unfollow by default

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations full_description.txt (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Russian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Vietnamese)

* New translations full_description.txt (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Vietnamese)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* make sure list in prefs are always mutable and nut null

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Russian)

* fix pronouns edge case

* add back fix for stretched images

closes sk22#636

* fix null pointer on missing default posting language

* fix default posting language not being applied

* bigger username hitbox

closes sk22#688

* fix rtl header username alignment

closes sk22#689

* New translations strings.xml (Ukrainian)

* New translations strings.xml (Ukrainian)

* hopefully fix crashes

closes sk22#692

* New translations strings.xml (Ukrainian)

* New translations full_description.txt (Ukrainian)

* fix pronoun crash

* New translations strings.xml (Persian)

* New translations strings.xml (Ukrainian)

* re-add true black mode

* asterisk can be a pronoun

* New translations strings.xml (Persian)

* true black mode fixes and clean-ups

* material 3 button background for switcher

* darker tab bar selected background

* better align follow/following button widths

* restore rainbow refresh colors

* fix search transition

* fix min width issue with switcher button

* fix no elevation when true black is enabled in light theme

* use statusForContent to determine spoilerRevealed

closes sk22#694

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* New translations strings.xml (Persian)

* fix profile tab bar in true black theme

* fix m3 default button style

closes sk22#697

* prettier role badges

closes sk22#663

* fix translate button spacing

closes sk22#655

* use m3 switches in dialogs

closes sk22#653

* implement color palette switcher

* fix color palettes being overwritten

* add display and notification settings

* clean up code

* per-account single notification setting

* add missing items to notification types

* add prefix replies setting

* add show replies/boosts and reply visibility

* add load/see new posts settings

* fix spectator mode missing spoiler padding

* add a bunch of display settings

* update fedinuke

* add content type settings

* add settings for local-onlu

* add missing settings items

* fix visibility button icon tint

* hopefully fix some crashes

* normalize padding above edit text

* apparently, some people don't like pills

closes sk22#706

* fix play button color

closes sk22#705
This commit is contained in:
sk22
2023-07-16 18:01:42 +02:00
committed by GitHub
parent 3cfea0e660
commit 7677ad39ca
744 changed files with 24873 additions and 13485 deletions

View File

@@ -12,7 +12,7 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -48,27 +48,22 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
user=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
filter=GetAccountStatuses.Filter.valueOf(getArguments().getString("filter"));
super.onAttach(activity);
}
@Override
protected void doLoadData(int offset, int count){
if(user==null) // TODO figure out why this happens
return;
currentRequest=new GetAccountStatuses(user.id, offset>0 ? getMaxID() : null, null, count, filter)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(getActivity()==null) return;
AccountSessionManager asm = AccountSessionManager.getInstance();
result=result.stream().filter(status -> {
// don't hide own posts in own profile
if (asm.isSelf(accountID, user) && asm.isSelf(accountID, status.account)) return true;
else return new StatusFilterPredicate(accountID, getFilterContext()).test(status);
}).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext(), user);
onDataLoaded(result, !empty);
}
})
.exec(accountID);
@@ -77,6 +72,7 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackground(null); // prevents unnecessary overdraw
}
@Override
@@ -86,20 +82,20 @@ public class AccountTimelineFragment extends StatusListFragment{
loadData();
}
protected void onStatusCreated(StatusCreatedEvent ev){
protected void onStatusCreated(Status status){
AccountSessionManager asm = AccountSessionManager.getInstance();
if(!asm.isSelf(accountID, ev.status.account) || !asm.isSelf(accountID, user))
if(!asm.isSelf(accountID, status.account) || !asm.isSelf(accountID, user))
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))
if(status.inReplyToAccountId!=null && !status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
return;
}else if(filter==GetAccountStatuses.Filter.MEDIA){
if(Optional.ofNullable(ev.status.mediaAttachments).map(List::isEmpty).orElse(true))
if(Optional.ofNullable(status.mediaAttachments).map(List::isEmpty).orElse(true))
return;
}
prependItems(Collections.singletonList(ev.status), true);
prependItems(Collections.singletonList(status), true);
if (isOnTop()) scrollToTop();
}
@@ -130,8 +126,8 @@ public class AccountTimelineFragment extends StatusListFragment{
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
protected FilterContext getFilterContext() {
return FilterContext.ACCOUNT;
}
@Override

View File

@@ -6,14 +6,10 @@ import android.content.res.Configuration;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -26,6 +22,7 @@ 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.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.DisplayItemsParent;
@@ -33,13 +30,16 @@ 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.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.PollOptionStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.SpoilerStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
@@ -54,6 +54,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -68,10 +69,11 @@ import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends RecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
public abstract class BaseStatusListFragment<T extends DisplayItemsParent> extends MastodonRecyclerFragment<T> implements PhotoViewerHost, ScrollableToTop, IsOnTop, HasFab, ProvidesAssistContent.ProvidesWebUri {
protected ArrayList<StatusDisplayItem> displayItems=new ArrayList<>();
protected DisplayItemsAdapter adapter;
protected String accountID;
@@ -96,7 +98,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
if(GlobalUserPreferences.disableMarquee){
if(GlobalUserPreferences.toolbarMarquee){
setTitleMarqueeEnabled(false);
setSubtitleMarqueeEnabled(false);
}
@@ -350,7 +352,11 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
public void getSelectorBounds(View view, Rect outRect){
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
int lastIndex = -1, firstIndex = -1;
list.getDecoratedBoundsWithMargins(view, outRect);
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
list.getDecoratedBoundsWithMargins(view, outRect);
}else{
outRect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
}
RecyclerView.ViewHolder holder=list.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder){
if(((StatusDisplayItem.Holder<?>) holder).getItem().getType()==StatusDisplayItem.Type.GAP){
@@ -427,6 +433,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
protected int getMainAdapterOffset(){
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
return mergeAdapter.getPositionForAdapter(adapter);
}
return 0;
}
@@ -438,6 +447,10 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
c.drawLine(0, y, parent.getWidth(), y, paint);
}
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return false;
}
public abstract void onItemClick(String id);
protected void updatePoll(String itemID, Status status, Poll poll){
@@ -525,38 +538,57 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
.exec(accountID);
}
public void onRevealSpoilerClick(TextStatusDisplayItem.Holder holder){
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID());
toggleSpoiler(status, holder.getItemID());
}
public void onRevealSpoilerClick(MediaGridStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
revealSpoiler(status, holder.getItemID());
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
Status status = holder.getItem().status;
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if (mediaGrid != null) {
if (!status.sensitiveRevealed) mediaGrid.revealSensitive();
else mediaGrid.hideSensitive();
} else {
// media grid's methods normally change the status' state - we still want to be able
// to do this if the media grid is not bound, tho - so, doing it ourselves here
status.sensitiveRevealed = !status.sensitiveRevealed;
}
holder.rebind();
}
protected void revealSpoiler(Status status, String itemID){
status.spoilerRevealed=true;
public void onSensitiveRevealed(MediaGridStatusDisplayItem.Holder holder) {
HeaderStatusDisplayItem.Holder header = findHolderOfType(holder.getItemID(), HeaderStatusDisplayItem.Holder.class);
if(header != null) header.rebind();
}
protected void toggleSpoiler(Status status, String itemID){
status.spoilerRevealed=!status.spoilerRevealed;
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
status.sensitiveRevealed = false;
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
if(spoiler!=null)
spoiler.rebind();
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
int index=displayItems.indexOf(spoilerItem);
if(status.spoilerRevealed){
displayItems.addAll(index+1, spoilerItem.contentItems);
adapter.notifyItemRangeInserted(index+1, spoilerItem.contentItems.size());
}else{
displayItems.subList(index+1, index+1+spoilerItem.contentItems.size()).clear();
adapter.notifyItemRangeRemoved(index+1, spoilerItem.contentItems.size());
}
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
if(text!=null)
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition()-getMainAdapterOffset());
HeaderStatusDisplayItem.Holder header=findHolderOfType(itemID, HeaderStatusDisplayItem.Holder.class);
if(header!=null)
header.rebind();
updateImagesSpoilerState(status, itemID);
}
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder){
Status status=holder.getItem().status;
status.spoilerRevealed=!status.spoilerRevealed;
if(!TextUtils.isEmpty(status.spoilerText)){
TextStatusDisplayItem.Holder text = findHolderOfType(holder.getItemID(), TextStatusDisplayItem.Holder.class);
if(text!=null){
adapter.notifyItemChanged(text.getAbsoluteAdapterPosition());
}
}
holder.rebind();
updateImagesSpoilerState(status, holder.getItemID());
list.invalidateItemDecorations();
}
public void onEnableExpandable(TextStatusDisplayItem.Holder holder, boolean expandable) {
@@ -575,30 +607,6 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
if (header != null) header.rebind();
}
protected void updateImagesSpoilerState(Status status, String itemID){
ArrayList<Integer> updatedPositions=new ArrayList<>();
MediaGridStatusDisplayItem.Holder mediaGrid=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
mediaGrid.setRevealed(status.spoilerRevealed);
updatedPositions.add(mediaGrid.getAbsoluteAdapterPosition()-getMainAdapterOffset());
}
int i=0;
for(StatusDisplayItem item:displayItems){
if(itemID.equals(item.parentID) && item instanceof MediaGridStatusDisplayItem && !updatedPositions.contains(i)){
adapter.notifyItemChanged(i);
}
i++;
}
}
public void onImageUpdated(MediaGridStatusDisplayItem.Holder holder, int index) {
holder.rebind();
MediaGridStatusDisplayItem.Holder mediaGrid = findHolderOfType(holder.getItemID(), MediaGridStatusDisplayItem.Holder.class);
if(mediaGrid!=null){
adapter.notifyItemChanged(mediaGrid.getAbsoluteAdapterPosition());
}
}
public void onGapClick(GapStatusDisplayItem.Holder item){}
public void onWarningClick(WarningFilteredStatusDisplayItem.Holder warning){
@@ -754,11 +762,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
public void rebuildAllDisplayItems(){
displayItems.clear();
for(T item:data){
displayItems.addAll(buildDisplayItems(item));
}
adapter.notifyDataSetChanged();
}
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
@Override
protected void onDataLoaded(List<T> d, boolean more) {
if (getContext()==null) return;
super.onDataLoaded(d, more);
// more available, but the page isn't even full yet? seems wrong, let's load some more
if (more && d.size() < itemsPerPage) preloader.onScrolledToLastItem();
if (more && d.size() < itemsPerPage) {
Log.d("BaseStatusListFragment", "doing the 'loading more things' thing!!! ipp: "+itemsPerPage+", items size: "+ d.size());
new Exception().printStackTrace();
preloader.onScrolledToLastItem();
}
}
protected class DisplayItemsAdapter extends UsableRecyclerView.Adapter<BindableViewHolder<StatusDisplayItem>> implements ImageLoaderRecyclerAdapter{
@@ -770,7 +793,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
@NonNull
@Override
public BindableViewHolder<StatusDisplayItem> onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return (BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent);
BindableViewHolder<StatusDisplayItem> holder=(BindableViewHolder<StatusDisplayItem>) StatusDisplayItem.createViewHolder(StatusDisplayItem.Type.values()[viewType & (~0x80000000)], getActivity(), parent, BaseStatusListFragment.this);
onModifyItemViewHolder(holder);
return holder;
}
@Override
@@ -801,15 +826,12 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
}
private class StatusListItemDecoration extends RecyclerView.ItemDecoration{
private Paint dividerPaint=new Paint(), hiddenMediaPaint=new Paint(Paint.ANTI_ALIAS_FLAG);
private Typeface mediumTypeface=Typeface.create("sans-serif-medium", Typeface.NORMAL);
private Layout mediaHiddenTitleLayout, mediaHiddenTextLayout, tapToRevealTextLayout;
private int currentMediaHiddenLayoutsWidth=0;
private Paint dividerPaint=new Paint();
{
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1));
dividerPaint.setStrokeWidth(V.dp(0.5f));
}
@Override
@@ -819,80 +841,23 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
View bottomSibling=parent.getChildAt(i+1);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
RecyclerView.ViewHolder siblingHolder=parent.getChildViewHolder(bottomSibling);
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh
&& (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP){
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) continue;
if(needDrawDivider(holder, siblingHolder)){
drawDivider(child, bottomSibling, holder, siblingHolder, parent, c, dividerPaint);
}
}
}
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed && TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
hiddenMediaPaint.setColor(0x80000000);
c.drawRect(child.getX(), child.getY(), child.getX()+child.getWidth(), child.getY()+child.getHeight(), hiddenMediaPaint);
}
}
private boolean needDrawDivider(RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
if(needDividerForExtraItem(holder.itemView, siblingHolder.itemView, holder, siblingHolder))
return true;
if(holder instanceof StatusDisplayItem.Holder<?> ih && siblingHolder instanceof StatusDisplayItem.Holder<?> sh){
// Do not draw dividers between hashtag and/or account rows
if((ih instanceof HashtagStatusDisplayItem.Holder || ih instanceof AccountStatusDisplayItem.Holder) && (sh instanceof HashtagStatusDisplayItem.Holder || sh instanceof AccountStatusDisplayItem.Holder))
return false;
if (!ih.getItem().isMainStatus && ih.getItem().hasDescendantNeighbor) return false;
return (!ih.getItemID().equals(sh.getItemID()) || sh instanceof ExtendedFooterStatusDisplayItem.Holder) && ih.getItem().getType()!=StatusDisplayItem.Type.GAP;
}
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof MediaGridStatusDisplayItem.Holder imgHolder){
if(!imgHolder.getItem().status.spoilerRevealed){
if(TextUtils.isEmpty(imgHolder.getItem().status.spoilerText)){
int listWidth=getListWidthForMediaLayout();
int width=Math.min(listWidth, UiUtils.MAX_WIDTH);
if(currentMediaHiddenLayoutsWidth!=width)
rebuildMediaHiddenLayouts(width-V.dp(32));
c.save();
float totalHeight;
boolean hiddenByAuthor=imgHolder.getItem().status.sensitive;
if(hiddenByAuthor)
totalHeight=mediaHiddenTitleLayout.getHeight()+mediaHiddenTextLayout.getHeight()+V.dp(8);
else
totalHeight=tapToRevealTextLayout.getHeight();
c.translate(child.getX()+V.dp(16), child.getY()+child.getHeight()/2f-totalHeight/2f);
if(hiddenByAuthor){
mediaHiddenTitleLayout.draw(c);
c.translate(0, mediaHiddenTitleLayout.getHeight()+V.dp(8));
mediaHiddenTextLayout.draw(c);
}else{
tapToRevealTextLayout.draw(c);
}
c.restore();
}
}
}
}
}
private void rebuildMediaHiddenLayouts(int width){
currentMediaHiddenLayoutsWidth=width;
String title=getString(R.string.sensitive_content);
TextPaint titlePaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
titlePaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray50));
titlePaint.setTextSize(V.dp(22));
titlePaint.setTypeface(mediumTypeface);
mediaHiddenTitleLayout=StaticLayout.Builder.obtain(title, 0, title.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
String tapToReveal=getString(R.string.tap_to_reveal);
tapToRevealTextLayout=StaticLayout.Builder.obtain(tapToReveal, 0, tapToReveal.length(), titlePaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.build();
TextPaint textPaint=new TextPaint(Paint.ANTI_ALIAS_FLAG);
textPaint.setColor(UiUtils.getThemeColor(getContext(), R.attr.colorGray200));
textPaint.setTextSize(V.dp(16));
String text=getString(R.string.sensitive_content_explain);
mediaHiddenTextLayout=StaticLayout.Builder.obtain(text, 0, text.length(), textPaint, width)
.setAlignment(Layout.Alignment.ALIGN_CENTER)
.setLineSpacing(V.dp(5), 1f)
.build();
return false;
}
}
}

View File

@@ -5,7 +5,8 @@ import android.net.Uri;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
@@ -39,8 +40,13 @@ public class BookmarkedStatusListFragment extends StatusListFragment{
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
protected void onRemoveAccountPostsEvent(RemoveAccountPostsEvent ev){
// no-op
}
@Override
protected FilterContext getFilterContext() {
return FilterContext.ACCOUNT;
}
@Override

View File

@@ -1,10 +1,18 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.content.res.TypedArray;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.media.MediaMetadataRetriever;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.view.Gravity;
import android.text.SpannableStringBuilder;
import android.text.style.BulletSpan;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -12,28 +20,34 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.UpdateAttachment;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment;
import org.parceler.Parcels;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import java.util.Collections;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
public class ComposeImageDescriptionFragment extends MastodonToolbarFragment implements OnBackPressedListener{
private static final String TAG="ComposeImageDescription";
private String accountID, attachmentID;
private EditText edit;
private Button saveButton;
private ImageView image;
private ContextThemeWrapper themeWrapper;
private PhotoViewer photoViewer;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -46,7 +60,13 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setTitle(R.string.edit_image);
themeWrapper=new ContextThemeWrapper(activity, R.style.Theme_Mastodon_Dark);
setTitle(R.string.add_alt_text);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
return super.onCreateView(themeWrapper.getSystemService(LayoutInflater.class), container, savedInstanceState);
}
@Override
@@ -54,14 +74,48 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
View view=inflater.inflate(R.layout.fragment_image_description, container, false);
edit=view.findViewById(R.id.edit);
ImageView image=view.findViewById(R.id.photo);
image=view.findViewById(R.id.photo);
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
if(width>0 && height>0){
// image.setAspectRatio(Math.max(1f, (float)width/height));
}
image.setOnClickListener(v->openPhotoViewer());
Uri uri=getArguments().getParcelable("uri");
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
Attachment.Type type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
if(type==Attachment.Type.IMAGE)
ViewImageLoader.load(image, null, new UrlImageLoaderRequest(uri, 1000, 1000));
else
loadVideoThumbIntoView(image, uri);
edit.setText(getArguments().getString("existingDescription"));
return view;
}
private void loadVideoThumbIntoView(ImageView target, Uri uri){
MastodonAPIController.runInBackground(()->{
Context context=getActivity();
if(context==null)
return;
try{
MediaMetadataRetriever mmr=new MediaMetadataRetriever();
mmr.setDataSource(context, uri);
Bitmap frame=mmr.getFrameAtTime(3_000_000);
mmr.release();
int size=Math.max(frame.getWidth(), frame.getHeight());
int maxSize=V.dp(250);
if(size>maxSize){
float factor=maxSize/(float)size;
frame=Bitmap.createScaledBitmap(frame, Math.round(frame.getWidth()*factor), Math.round(frame.getHeight()*factor), true);
}
Bitmap finalFrame=frame;
target.post(()->target.setImageBitmap(finalFrame));
}catch(Exception x){
Log.w(TAG, "loadVideoThumbIntoView: error getting video frame", x);
}
});
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
@@ -71,43 +125,114 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment{
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
TypedArray ta=getActivity().obtainStyledAttributes(new int[]{R.attr.secondaryButtonStyle});
int buttonStyle=ta.getResourceId(0, 0);
ta.recycle();
saveButton=new Button(getActivity(), null, 0, buttonStyle);
saveButton.setText(R.string.save);
saveButton.setOnClickListener(this::onSaveClick);
FrameLayout wrap=new FrameLayout(getActivity());
wrap.addView(saveButton, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.TOP|Gravity.LEFT));
wrap.setPadding(V.dp(16), V.dp(4), V.dp(16), V.dp(8));
wrap.setClipToPadding(false);
MenuItem item=menu.add(R.string.publish);
item.setActionView(wrap);
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
inflater.inflate(R.menu.compose_image_description, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.help){
SpannableStringBuilder msg=new SpannableStringBuilder(getText(R.string.alt_text_help));
BulletSpan[] spans=msg.getSpans(0, msg.length(), BulletSpan.class);
for(BulletSpan span:spans){
BulletSpan betterSpan;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.Q)
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface));
else
betterSpan=new BulletSpan(V.dp(10), UiUtils.getThemeColor(themeWrapper, R.attr.colorM3OnSurface), V.dp(1.5f));
msg.setSpan(betterSpan, msg.getSpanStart(span), msg.getSpanEnd(span), msg.getSpanFlags(span));
msg.removeSpan(span);
}
new M3AlertDialogBuilder(themeWrapper)
.setTitle(R.string.what_is_alt_text)
.setMessage(msg)
.setPositiveButton(R.string.ok, null)
.show();
}
return true;
}
private void onSaveClick(View v){
new UpdateAttachment(attachmentID, edit.getText().toString().trim())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Attachment result){
Bundle r=new Bundle();
r.putParcelable("attachment", Parcels.wrap(result));
setResult(true, r);
Nav.finish(ComposeImageDescriptionFragment.this);
}
@Override
public boolean onBackPressed(){
deliverResult();
return false;
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, false)
.exec(accountID);
@Override
protected LayoutInflater getToolbarLayoutInflater(){
return LayoutInflater.from(themeWrapper);
}
private void deliverResult(){
Bundle r=new Bundle();
r.putString("text", edit.getText().toString().trim());
r.putString("attachment", attachmentID);
setResult(true, r);
}
private void openPhotoViewer(){
Attachment fakeAttachment=new Attachment();
fakeAttachment.id="local";
fakeAttachment.type=Attachment.Type.valueOf(getArguments().getString("attachmentType"));
int width=getArguments().getInt("width", 0);
int height=getArguments().getInt("height", 0);
Uri uri=getArguments().getParcelable("uri");
fakeAttachment.url=uri.toString();
fakeAttachment.meta=new Attachment.Metadata();
fakeAttachment.meta.width=width;
fakeAttachment.meta.height=height;
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
@Override
public void setPhotoViewVisibility(int index, boolean visible){
image.setAlpha(visible ? 1f : 0f);
}
@Override
public boolean startPhotoViewTransition(int index, @NonNull Rect outRect, @NonNull int[] outCornerRadius){
int[] pos={0, 0};
image.getLocationOnScreen(pos);
outRect.set(pos[0], pos[1], pos[0]+image.getWidth(), pos[1]+image.getHeight());
image.setElevation(1f);
return true;
}
@Override
public void setTransitioningViewTransform(float translateX, float translateY, float scale){
image.setTranslationX(translateX);
image.setTranslationY(translateY);
image.setScaleX(scale);
image.setScaleY(scale);
}
@Override
public void endPhotoViewTransition(){
Drawable d=image.getDrawable();
image.setImageDrawable(null);
image.setImageDrawable(d);
image.setTranslationX(0f);
image.setTranslationY(0f);
image.setScaleX(1f);
image.setScaleY(1f);
image.setElevation(0f);
}
@Nullable
@Override
public Drawable getPhotoViewCurrentDrawable(int index){
return image.getDrawable();
}
@Override
public void photoViewerDismissed(){
photoViewer=null;
}
@Override
public void onRequestPermissions(String[] permissions){
}
});
photoViewer.removeMenu();
}
}

View File

@@ -16,7 +16,6 @@ import android.view.MotionEvent;
import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
@@ -35,10 +34,11 @@ import androidx.recyclerview.widget.RecyclerView;
import com.hootsuite.nachos.NachoTextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.ListTimeline;
@@ -52,6 +52,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import me.grishka.appkit.api.Callback;
@@ -59,7 +60,7 @@ import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition> implements ScrollableToTop {
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
private String accountID;
private TimelinesAdapter adapter;
private final ItemTouchHelper itemTouchHelper;
@@ -121,7 +122,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
super.onViewCreated(view, savedInstanceState);
itemTouchHelper.attachToRecyclerView(list);
refreshLayout.setEnabled(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
}
@Override
@@ -187,7 +188,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@@ -200,10 +201,12 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
}
private void saveTimelines() {
updated = true;
GlobalUserPreferences.pinnedTimelines.put(accountID, data.size() > 0 ? data : List.of(TimelineDefinition.HOME_TIMELINE));
GlobalUserPreferences.save();
}
updated=true;
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
prefs.timelines=data;
prefs.save();
}
private void removeTimeline(int position) {
data.remove(position);
@@ -214,7 +217,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
updateOptionsMenu();
}
@@ -256,7 +259,8 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
Context ctx = getContext();
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
Button advancedBtn = view.findViewById(R.id.advanced);
View divider = view.findViewById(R.id.divider);
Button advancedBtn = view.findViewById(R.id.advanced);
EditText editText = view.findViewById(R.id.input);
if (item != null) editText.setText(item.getCustomTitle());
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
@@ -264,11 +268,12 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
view.findViewById(R.id.divider).setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
advancedBtn.setOnClickListener(l -> {
advancedBtn.setSelected(!advancedBtn.isSelected());
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
UiUtils.beginLayoutTransition((ViewGroup) view);
});
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
@@ -281,8 +286,9 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
if (item != null) {
tagMain.setText(item.getHashtagName());
boolean hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny());
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
if (item.isHashtagLocalOnly()) {
localOnlySwitch.setChecked(true);
@@ -291,7 +297,8 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
if (hasAdvanced) {
advancedBtn.setSelected(true);
advancedBtn.setText(R.string.sk_advanced_options_hide);
tagWrap.setVisibility(View.VISIBLE);
tagWrap.setVisibility(View.VISIBLE);
divider.setVisibility(View.VISIBLE);
}
}

View File

@@ -5,7 +5,7 @@ import android.net.Uri;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Status;
@@ -39,8 +39,8 @@ public class FavoritedStatusListFragment extends StatusListFragment{
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
protected FilterContext getFilterContext() {
return FilterContext.ACCOUNT;
}
@Override

View File

@@ -0,0 +1,64 @@
package org.joinmastodon.android.fragments;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
public class FeaturedHashtagsListFragment extends BaseStatusListFragment<Hashtag>{
private Account account;
private String accountID;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
onDataLoaded(getArguments().getParcelableArrayList("hashtags").stream().map(p->(Hashtag)Parcels.unwrap(p)).collect(Collectors.toList()), false);
setTitle(R.string.hashtags);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Hashtag s){
return Collections.singletonList(new HashtagStatusDisplayItem(s.name, this, s));
}
@Override
protected void addAccountToKnown(Hashtag s){
}
@Override
public void onItemClick(String id){
UiUtils.openHashtagTimeline(getActivity(), accountID, id, data.stream().filter(h -> Objects.equals(h.name, id)).findAny().map(h -> h.following).orElse(null));
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
// no-op
}
@Override
public Uri getWebUri(Uri.Builder base){
return null; // TODO
}
}

View File

@@ -48,7 +48,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
public class FollowRequestsListFragment extends MastodonRecyclerFragment<FollowRequestsListFragment.AccountWrapper> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -254,7 +254,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
@@ -278,7 +278,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
actionWrap.setVisibility(View.VISIBLE);
acceptWrap.setVisibility(View.GONE);
rejectWrap.setVisibility(View.GONE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
}
}
@@ -313,6 +313,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
private void onFollowRequestButtonClick(View v) {
itemView.setHasTransientState(true);
UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> {
if(getContext()==null) return;
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter = getBindingAdapter();
@@ -328,6 +329,7 @@ public class FollowRequestsListFragment extends RecyclerFragment<FollowRequestsL
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
if(getContext()==null) return;
itemView.setHasTransientState(false);
relationships.put(item.account.id, rel);
rebind();

View File

@@ -21,7 +21,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
public class FollowedHashtagsFragment extends MastodonRecyclerFragment<Hashtag> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String nextMaxID;
private String accountID;
@@ -47,7 +47,7 @@ public class FollowedHashtagsFragment extends RecyclerFragment<Hashtag> implemen
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
}
@Override

View File

@@ -1,5 +1,6 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
@@ -24,4 +25,8 @@ public interface HasAccountID {
default Optional<Instance> getInstance() {
return getSession().getInstance();
}
default AccountLocalPreferences getLocalPrefs() {
return AccountSessionManager.get(getAccountID()).getLocalPreferences();
}
}

View File

@@ -0,0 +1,7 @@
package org.joinmastodon.android.fragments;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
public interface HasElevationOnScrollListener {
ElevationOnScrollListener getElevationOnScrollListener();
}

View File

@@ -17,7 +17,7 @@ import org.joinmastodon.android.api.requests.tags.GetHashtag;
import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed;
import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline;
import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
@@ -126,7 +126,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly)
currentRequest=new GetHashtagTimeline(hashtag, offset==0 ? null : getMaxID(), null, count, any, all, none, localOnly, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@@ -160,12 +160,12 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment {
@Override
protected void onSetFabBottomInset(int inset){
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(24)+inset;
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+inset;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override

View File

@@ -1,21 +1,21 @@
package org.joinmastodon.android.fragments;
import android.annotation.SuppressLint;
import android.app.Fragment;
import android.app.NotificationManager;
import android.app.assist.AssistContent;
import android.graphics.Outline;
import android.os.Build;
import android.os.Bundle;
import android.service.notification.StatusBarNotification;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.Nullable;
@@ -23,26 +23,27 @@ import androidx.annotation.Nullable;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.NotificationReceivedEvent;
import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.fragments.discover.DiscoverFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.TabBar;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Optional;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.api.Callback;
@@ -59,42 +60,38 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private FragmentRootLinearLayout content;
private HomeTabFragment homeTabFragment;
private NotificationsFragment notificationsFragment;
private DiscoverFragment searchFragment;
private DiscoverFragment discoverFragment;
private ProfileFragment profileFragment;
private TabBar tabBar;
private View tabBarWrap;
private ImageView tabBarAvatar;
private ImageView notificationTabIcon;
@IdRes
private int currentTab=R.id.tab_home;
private TextView notificationsBadge;
private String accountID;
private boolean isPleroma;
private boolean isAkkoma;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
accountID=getArguments().getString("account");
setTitle(R.string.sk_app_name);
isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance()
.map(Instance::isAkkoma)
.orElse(false);
isAkkoma = getInstance().map(Instance::isAkkoma).orElse(false);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
// TODO: clean up
if(savedInstanceState==null){
Bundle args=new Bundle();
args.putString("account", accountID);
homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args);
args=new Bundle(args);
args.putBoolean("disableDiscover", isPleroma);
args.putBoolean("disableDiscover", isAkkoma);
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
discoverFragment=new DiscoverFragment();
discoverFragment.setArguments(args);
notificationsFragment=new NotificationsFragment();
notificationsFragment.setArguments(args);
args=new Bundle(args);
@@ -104,6 +101,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
profileFragment.setArguments(args);
}
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Nullable
@@ -121,24 +125,28 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
tabBar.setListeners(this::onTabSelected, this::onTabLongClick);
tabBarWrap=content.findViewById(R.id.tabbar_wrap);
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){
@Override
public void getOutline(View view, Outline outline){
outline.setOval(0, 0, view.getWidth(), view.getHeight());
// this one's for the pill haters (https://m3.material.io/components/navigation-bar/overview)
if (GlobalUserPreferences.disableM3PillActiveIndicator) {
for(int i=0; i<tabBar.getChildCount(); i++){
ViewGroup f=(ViewGroup) tabBar.getChildAt(i);
f.setBackgroundResource(R.drawable.bg_tabbar_tab_ripple);
}
});
tabBar.findViewById(R.id.tab_profile).setBackgroundResource(R.drawable.bg_tab_profile);
}
tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava);
tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL);
tabBarAvatar.setClipToOutline(true);
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28)));
ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
notificationTabIcon=content.findViewById(R.id.tab_notifications);
updateNotificationBadge();
notificationsBadge=tabBar.findViewById(R.id.notifications_badge);
notificationsBadge.setVisibility(View.GONE);
if(savedInstanceState==null){
getChildFragmentManager().beginTransaction()
.add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, discoverFragment).hide(discoverFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment)
.add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment)
.commit();
@@ -165,7 +173,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onViewStateRestored(savedInstanceState);
if(savedInstanceState==null) return;
homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment");
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
discoverFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
currentTab=savedInstanceState.getInt("selectedTab");
@@ -173,7 +181,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
Fragment current=fragmentForTab(currentTab);
getChildFragmentManager().beginTransaction()
.hide(homeTabFragment)
.hide(searchFragment)
.hide(discoverFragment)
.hide(notificationsFragment)
.hide(profileFragment)
.show(current)
@@ -189,7 +197,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean wantsLightStatusBar(){
return currentTab!=R.id.tab_profile && !UiUtils.isDarkTheme();
return !UiUtils.isDarkTheme();
}
@Override
@@ -201,14 +209,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
WindowInsets topOnlyInsets=insets.replaceSystemWindowInsets(0, insets.getSystemWindowInsetTop(), 0, 0);
homeTabFragment.onApplyWindowInsets(topOnlyInsets);
searchFragment.onApplyWindowInsets(topOnlyInsets);
discoverFragment.onApplyWindowInsets(topOnlyInsets);
notificationsFragment.onApplyWindowInsets(topOnlyInsets);
profileFragment.onApplyWindowInsets(topOnlyInsets);
}
@@ -217,7 +225,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(tab==R.id.tab_home){
return homeTabFragment;
}else if(tab==R.id.tab_search){
return searchFragment;
return discoverFragment;
}else if(tab==R.id.tab_notifications){
return notificationsFragment;
}else if(tab==R.id.tab_profile){
@@ -235,11 +243,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
private void onTabSelected(@IdRes int tab){
Fragment newFragment=fragmentForTab(tab);
if(tab==currentTab){
if (tab == R.id.tab_search)
searchFragment.onSelect();
else if(newFragment instanceof ScrollableToTop scrollable)
scrollable.scrollToTop();
if(tab==currentTab && newFragment instanceof ScrollableToTop scrollable) {
scrollable.scrollToTop();
return;
}
getChildFragmentManager().beginTransaction().hide(fragmentForTab(currentTab)).show(newFragment).commit();
@@ -247,7 +252,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@@ -258,7 +262,6 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
((DiscoverFragment) newFragment).loadData();
}else if(newFragment instanceof NotificationsFragment){
((NotificationsFragment) newFragment).loadData();
// TODO make an interface?
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
for (StatusBarNotification notification : nm.getActiveNotifications()) {
if (accountID.equals(notification.getTag())) {
@@ -276,6 +279,11 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
}
new AccountSwitcherSheet(getActivity(), this).show();
return true;
} else if(tab==R.id.tab_search){
tabBar.selectTab(R.id.tab_search);
onTabSelected(R.id.tab_search);
discoverFragment.openSearch();
return true;
}
return false;
}
@@ -285,7 +293,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if(currentTab==R.id.tab_profile)
if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search)
if (searchFragment.onBackPressed()) return true;
if (discoverFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
@@ -300,52 +308,79 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
super.onSaveInstanceState(outState);
outState.putInt("selectedTab", currentTab);
if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment);
if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment);
if (discoverFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", discoverFragment);
if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment);
if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
}
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Optional<Instance> instance = session.getInstance();
if (instance.isEmpty()) return; // avoiding incompatibility with akkoma
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {
if (notifications.size() > 0) {
try {
long newestId = Long.parseLong(notifications.get(0).id);
long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId);
setNotificationBadge(newestId > lastSeenId);
} catch (Exception ignored) {
setNotificationBadge(false);
}
}
}
@Override
public void onError(ErrorResponse error) {
setNotificationBadge(false);
}
}).exec(accountID);
@Override
protected void onShown(){
super.onShown();
reloadNotificationsForUnreadCount();
}
public void setNotificationBadge(boolean badge) {
notificationTabIcon.setImageResource(badge
? R.drawable.ic_fluent_alert_28_selector_badged
: R.drawable.ic_fluent_alert_28_selector);
public void reloadNotificationsForUnreadCount(){
List<Notification>[] notifications=new List[]{null};
String[] marker={null};
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
marker[0]=m;
if(notifications[0]!=null){
updateUnreadCount(notifications[0], marker[0]);
}
});
AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, false, true, new Callback<>(){
@Override
public void onSuccess(PaginatedResponse<List<Notification>> result){
notifications[0]=result.items;
if(marker[0]!=null)
updateUnreadCount(notifications[0], marker[0]);
}
@Override
public void onError(ErrorResponse error){}
});
}
@SuppressLint("DefaultLocale")
private void updateUnreadCount(List<Notification> notifications, String marker){
if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){
V.setVisibilityAnimated(notificationsBadge, View.GONE);
}else{
V.setVisibilityAnimated(notificationsBadge, View.VISIBLE);
if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){
notificationsBadge.setText(String.format("%d+", notifications.size()));
}else{
int count=0;
for(Notification n:notifications){
if(n.id.equals(marker))
break;
count++;
}
notificationsBadge.setText(String.format("%d", count));
}
}
}
@Subscribe
public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) {
if (notificationReceivedEvent.account.equals(accountID)) setNotificationBadge(true);
public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(ev.clearUnread)
V.setVisibilityAnimated(notificationsBadge, View.GONE);
}
@Subscribe
public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) {
setNotificationBadge(false);
public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){
if(!ev.accountID.equals(accountID))
return;
if(homeTabFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
&& lf instanceof BaseStatusListFragment<?> homeTimelineFragment)
homeTimelineFragment.rebuildAllDisplayItems();
if(notificationsFragment.getCurrentFragment() instanceof LoaderFragment lf && lf.loaded
&& lf instanceof BaseStatusListFragment<?> l)
l.rebuildAllDisplayItems();
}
@Override

View File

@@ -12,6 +12,7 @@ import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.assist.AssistContent;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -43,10 +44,12 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.announcements.GetAnnouncements;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.HashtagUpdatedEvent;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Announcement;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
@@ -55,6 +58,7 @@ import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.SimpleViewHolder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.Collection;
@@ -72,8 +76,9 @@ import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.CubicBezierInterpolator;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent {
public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, OnBackPressedListener, HasFab, ProvidesAssistContent, HasElevationOnScrollListener {
private static final int ANNOUNCEMENTS_RESULT = 654;
private String accountID;
@@ -91,7 +96,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private PopupMenu switcherPopup;
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
private List<TimelineDefinition> timelineDefinitions;
private List<TimelineDefinition> timelinesList;
private int count;
private Fragment[] fragments;
private FrameLayout[] tabViews;
@@ -102,19 +107,20 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
private View overflowActionView = null;
private boolean announcementsBadged, settingsBadged;
private ImageButton fab;
private ElevationOnScrollListener elevationOnScrollListener;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();
fragments = new Fragment[count];
tabViews = new FrameLayout[count];
timelines = new TimelineDefinition[count];
timelinesList=AccountSessionManager.get(accountID).getLocalPreferences().timelines;
assert timelinesList!=null;
if(timelinesList.isEmpty()) timelinesList=List.of(TimelineDefinition.HOME_TIMELINE);
count=timelinesList.size();
fragments=new Fragment[count];
tabViews=new FrameLayout[count];
timelines=new TimelineDefinition[count];
}
@Override
@@ -125,7 +131,11 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle bundle) {
FragmentRootLinearLayout rootView = new FragmentRootLinearLayout(getContext());
rootView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
FrameLayout view = new FrameLayout(getContext());
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
rootView.addView(view);
inflater.inflate(R.layout.compose_fab, view);
fab = view.findViewById(R.id.fab);
fab.setOnClickListener(this::onFabClick);
@@ -140,8 +150,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
args.putBoolean("__disable_fab", true);
args.putBoolean("onlyPosts", true);
for (int i = 0; i < timelineDefinitions.size(); i++) {
TimelineDefinition tl = timelineDefinitions.get(i);
for (int i=0; i < timelinesList.size(); i++) {
TimelineDefinition tl = timelinesList.get(i);
fragments[i] = tl.getFragment();
timelines[i] = tl;
}
@@ -168,7 +178,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
overflowActionView.setOnClickListener(l -> overflowPopup.show());
overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener());
return view;
return rootView;
}
@SuppressLint("ClickableViewAccessibility")
@@ -243,6 +253,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
});
}
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar());
if(GithubSelfUpdater.needSelfUpdating()){
updateUpdateState(GithubSelfUpdater.getInstance().getState());
}
@@ -289,6 +301,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
}).exec(accountID);
}
public ElevationOnScrollListener getElevationOnScrollListener() {
return elevationOnScrollListener;
}
private void onFabClick(View v){
if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment<?> l) {
l.onFabClick(v);
@@ -466,10 +482,19 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
&& fabulous.isScrolling();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (elevationOnScrollListener != null) elevationOnScrollListener.setViews(getToolbar());
}
private void updateSwitcherIcon(int i) {
timelineIcon.setImageResource(timelines[i].getIcon().iconRes);
timelineTitle.setText(timelines[i].getTitle(getContext()));
showFab();
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f) {
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
}
}
@Override
@@ -484,7 +509,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
getToolbar().post(() -> overflowPopup.show());
return true;
} else if (id == R.id.settings || id == R.id.settings_action) {
Nav.go(getActivity(), SettingsFragment.class, args);
Nav.go(getActivity(), SettingsMainFragment.class, args);
} else if (id == R.id.announcements || id == R.id.announcements_action) {
Nav.goForResult(getActivity(), AnnouncementsFragment.class, args, ANNOUNCEMENTS_RESULT, this);
} else if (id == R.id.edit_timelines) {
@@ -633,8 +658,8 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
@Override
protected void onShown() {
super.onShown();
Object pinnedTimelines = GlobalUserPreferences.pinnedTimelines.get(accountID);
if (pinnedTimelines != null && timelineDefinitions != pinnedTimelines) UiUtils.restartApp();
Object timelines = AccountSessionManager.get(accountID).getLocalPreferences().timelines;
if (timelines != null && timelinesList!= timelines) UiUtils.restartApp();
}
@Override
@@ -698,6 +723,10 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
return hashtagsItems.values();
}
public Fragment getCurrentFragment() {
return fragments[pager.getCurrentItem()];
}
public ImageButton getFab() {
return fab;
}

View File

@@ -11,11 +11,13 @@ import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineMarkers;
import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -49,8 +51,9 @@ public class HomeTimelineFragment extends StatusListFragment {
}
private boolean typeFilterPredicate(Status s) {
return (GlobalUserPreferences.showReplies || s.inReplyToId == null) &&
(GlobalUserPreferences.showBoosts || s.reblog == null);
AccountLocalPreferences lp=getLocalPrefs();
return (lp.showReplies || s.inReplyToId == null) &&
(lp.showBoosts || s.reblog == null);
}
private List<Status> filterPosts(List<Status> items) {
@@ -110,7 +113,7 @@ public class HomeTimelineFragment extends StatusListFragment {
new SaveMarkers(topPostID, null)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SaveMarkers.Response result){
public void onSuccess(TimelineMarkers result){
}
@Override
@@ -123,8 +126,8 @@ public class HomeTimelineFragment extends StatusListFragment {
}
}
public void onStatusCreated(StatusCreatedEvent ev){
prependItems(Collections.singletonList(ev.status), true);
public void onStatusCreated(Status status){
prependItems(Collections.singletonList(status), true);
}
private void loadNewPosts(){
@@ -134,7 +137,7 @@ public class HomeTimelineFragment extends StatusListFragment {
// we'll get the currently topmost post as last in the response. This way we know there's no gap
// between the existing and newly loaded parts of the timeline.
String sinceID=data.size()>1 ? data.get(1).id : "1";
currentRequest=new GetHomeTimeline(null, null, 20, sinceID)
currentRequest=new GetHomeTimeline(null, null, 20, sinceID, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
@@ -151,8 +154,7 @@ public class HomeTimelineFragment extends StatusListFragment {
result.get(result.size()-1).hasGapAfter=true;
toAdd=result;
}
StatusFilterPredicate filterPredicate=new StatusFilterPredicate(accountID, getFilterContext());
toAdd=toAdd.stream().filter(filterPredicate).collect(Collectors.toList());
AccountSessionManager.get(accountID).filterStatuses(toAdd, getFilterContext());
if(!toAdd.isEmpty()){
prependItems(toAdd, true);
if (parent != null && GlobalUserPreferences.showNewPostsButton) parent.showNewPostsButton();
@@ -169,7 +171,7 @@ public class HomeTimelineFragment extends StatusListFragment {
.exec(accountID);
if (parent.getParentFragment() instanceof HomeFragment homeFragment) {
homeFragment.updateNotificationBadge();
homeFragment.reloadNotificationsForUnreadCount();
}
}
@@ -182,7 +184,7 @@ public class HomeTimelineFragment extends StatusListFragment {
V.setVisibilityAnimated(item.text, View.GONE);
GapStatusDisplayItem gap=item.getItem();
dataLoading=true;
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null)
currentRequest=new GetHomeTimeline(item.getItemID(), null, 20, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Status> result){
@@ -239,6 +241,7 @@ public class HomeTimelineFragment extends StatusListFragment {
insertedPosts.add(s);
}
}
AccountSessionManager.get(accountID).filterStatuses(insertedPosts, getFilterContext());
if(targetList.isEmpty()){
// oops. We didn't add new posts, but at least we know there are none.
adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos);
@@ -285,8 +288,8 @@ public class HomeTimelineFragment extends StatusListFragment {
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
protected FilterContext getFilterContext() {
return FilterContext.HOME;
}
@Override

View File

@@ -18,7 +18,7 @@ import org.joinmastodon.android.api.requests.lists.UpdateList;
import org.joinmastodon.android.api.requests.timelines.GetListTimeline;
import org.joinmastodon.android.events.ListDeletedEvent;
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.TimelineDefinition;
@@ -134,7 +134,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override
protected void doLoadData(int offset, int count) {
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null)
currentRequest=new GetListTimeline(listID, offset==0 ? null : getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this) {
@Override
public void onSuccess(List<Status> result) {
@@ -167,8 +167,8 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.HOME;
protected FilterContext getFilterContext() {
return FilterContext.HOME;
}
@Override

View File

@@ -42,7 +42,7 @@ import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class ListsFragment extends RecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private String profileAccountId;
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
@@ -80,7 +80,7 @@ public class ListsFragment extends RecyclerFragment<ListTimeline> implements Scr
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 0.5f, 56, 16));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
}
@Override

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.View;
import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import androidx.annotation.CallSuper;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public abstract class MastodonRecyclerFragment<T> extends BaseRecyclerFragment<T>{
protected ElevationOnScrollListener elevationOnScrollListener;
public MastodonRecyclerFragment(int perPage){
super(perPage);
}
public MastodonRecyclerFragment(int layout, int perPage){
super(layout, perPage);
}
protected List<View> getViewsForElevationEffect(){
Toolbar toolbar=getToolbar();
return toolbar!=null ? Collections.singletonList(toolbar) : Collections.emptyList();
}
@Override
@CallSuper
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if (getParentFragment() instanceof HasElevationOnScrollListener elevator)
list.addOnScrollListener(elevator.getElevationOnScrollListener());
else if(wantsElevationOnScrollEffect())
list.addOnScrollListener(elevationOnScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, getViewsForElevationEffect()));
if(refreshLayout!=null)
setRefreshLayoutColors(refreshLayout);
}
@Override
@CallSuper
protected void onUpdateToolbar(){
super.onUpdateToolbar();
if(elevationOnScrollListener!=null){
elevationOnScrollListener.setViews(getViewsForElevationEffect());
}
}
protected boolean wantsElevationOnScrollEffect(){
return true;
}
public List<T> getData() {
return data;
}
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
List<Integer> colors = new ArrayList<>(Arrays.asList(
UiUtils.isDarkTheme() ? R.color.primary_200 : R.color.primary_600,
UiUtils.isDarkTheme() ? R.color.red_primary_200 : R.color.red_primary_600,
UiUtils.isDarkTheme() ? R.color.green_primary_200 : R.color.green_primary_600,
UiUtils.isDarkTheme() ? R.color.blue_primary_200 : R.color.blue_primary_600,
UiUtils.isDarkTheme() ? R.color.purple_200 : R.color.purple_600
));
int primary = UiUtils.getThemeColorRes(l.getContext(),
UiUtils.isDarkTheme() ? R.attr.colorPrimary200 : R.attr.colorPrimary600);
if (!colors.contains(primary)) colors.add(0, primary);
int offset = colors.indexOf(primary);
int[] sorted = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
sorted[i] = colors.get((i + offset) % colors.size());
}
l.setColorSchemeResources(sorted);
int colorBackground=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Background);
int colorPrimary=UiUtils.getThemeColor(l.getContext(), R.attr.colorM3Primary);
l.setProgressBackgroundColorSchemeColor(UiUtils.alphaBlendColors(colorBackground, colorPrimary, 0.11f));
}
}

View File

@@ -11,6 +11,15 @@ import androidx.annotation.CallSuper;
import me.grishka.appkit.fragments.ToolbarFragment;
public abstract class MastodonToolbarFragment extends ToolbarFragment{
public MastodonToolbarFragment(){
super();
}
protected MastodonToolbarFragment(int layout){
super(layout);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);

View File

@@ -3,6 +3,7 @@ package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -24,6 +25,9 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetFollowRequests;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FollowRequestHandledEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
@@ -31,6 +35,8 @@ 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 org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import me.grishka.appkit.Nav;
@@ -38,15 +44,19 @@ import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent {
public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop, ProvidesAssistContent, HasElevationOnScrollListener {
private TabLayout tabLayout;
TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private View tabsDivider;
private TabLayoutMediator tabLayoutMediator;
String unreadMarker, realUnreadMarker;
private MenuItem markAllReadItem;
private NotificationsListFragment allNotificationsFragment, mentionsFragment;
private ElevationOnScrollListener elevationOnScrollListener;
private String accountID;
@Override
@@ -72,11 +82,19 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
setTitle(R.string.notifications);
}
@Override
public void onShown() {
super.onShown();
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.notifications, menu);
menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications);
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests);
markAllReadItem=menu.findItem(R.id.mark_all_read);
updateMarkAllReadButton();
UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read);
}
@Override
@@ -93,15 +111,40 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
}
});
return true;
} else if (item.getItemId() == R.id.mark_all_read) {
markAsRead();
if (getCurrentFragment() instanceof NotificationsListFragment nlf) {
nlf.resetUnreadBackground();
}
return true;
}
return false;
}
void markAsRead(){
if(allNotificationsFragment.getData().isEmpty()) return;
String id=allNotificationsFragment.getData().get(0).id;
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
new SaveMarkers(null, id).exec(accountID);
if (allNotificationsFragment.isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(id).exec(accountID);
}
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
realUnreadMarker=id;
updateMarkAllReadButton();
}
}
public void updateMarkAllReadButton(){
markAllReadItem.setVisible(!allNotificationsFragment.getData().isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(allNotificationsFragment.getData().get(0).id));
}
@Override
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);
tabsDivider=view.findViewById(R.id.tabs_divider);
pager=view.findViewById(R.id.pager);
UiUtils.reduceSwipeSensitivity(pager);
@@ -119,7 +162,7 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
}
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {}
@@ -139,6 +182,8 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
public void onPageSelected(int position){
if (elevationOnScrollListener != null && getCurrentFragment() instanceof IsOnTop f)
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
if(position==0)
return;
Fragment _page=getFragmentForPage(position);
@@ -176,7 +221,6 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
case 1 -> R.string.mentions;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
@@ -184,6 +228,28 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar(), tabLayout);
elevationOnScrollListener.setDivider(tabsDivider);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (elevationOnScrollListener == null) return;
elevationOnScrollListener.setViews(getToolbar(), tabLayout);
if (getCurrentFragment() instanceof IsOnTop f) {
elevationOnScrollListener.handleScroll(getContext(), f.isOnTop());
}
}
@Override
public ElevationOnScrollListener getElevationOnScrollListener() {
return elevationOnScrollListener;
}
public void refreshFollowRequestsBadge() {
new GetFollowRequests(null, 1).setCallback(new Callback<>() {
@Override
@@ -228,6 +294,10 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc
};
}
public Fragment getCurrentFragment() {
return getFragmentForPage(pager.getCurrentItem());
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(getFragmentForPage(pager.getCurrentItem()), assistContent);

View File

@@ -1,6 +1,9 @@
package org.joinmastodon.android.fragments;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
@@ -10,50 +13,44 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.joinmastodon.android.utils.ObjectIdComparator;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
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.MergeRecyclerAdapter;
import me.grishka.appkit.views.FragmentRootLinearLayout;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
private boolean onlyMentions;
private boolean onlyPosts;
private String maxID;
private final DiscoverInfoBannerHelper bannerHelper = new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS);
private boolean reloadingFromCache;
private DiscoverInfoBannerHelper bannerHelper;
@Override
protected boolean wantsComposeButton() {
@@ -64,6 +61,13 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
E.register(this);
if(savedInstanceState!=null){
onlyMentions=savedInstanceState.getBoolean("onlyMentions", false);
onlyPosts=savedInstanceState.getBoolean("onlyPosts", false);
}
if (onlyPosts) {
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.POST_NOTIFICATIONS, accountID);
}
}
@Override
@@ -77,59 +81,35 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
super.onAttach(activity);
onlyMentions=getArguments().getBoolean("onlyMentions", false);
onlyPosts=getArguments().getBoolean("onlyPosts", false);
}
@Override
public void onRefresh() {
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment notificationsFragment) {
notificationsFragment.refreshFollowRequestsBadge();
}
setTitle(R.string.notifications);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null :
n.report.targetAccount;
Emoji emoji = new Emoji();
if(n.emojiUrl!=null){
emoji.shortcode=n.emoji.substring(1,n.emoji.length()-1);
emoji.url=n.emojiUrl;
emoji.staticUrl=n.emojiUrl;
emoji.visibleInPicker=false;
NotificationHeaderStatusDisplayItem titleItem;
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
titleItem=null;
}else{
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
}
if (n.type == Notification.Type.FOLLOW_REQUEST) {
ArrayList<StatusDisplayItem> items = new ArrayList<>();
items.add(titleItem);
items.add(new AccountCardStatusDisplayItem(n.id, this, n.account, n));
return items;
}
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.notification_boosted);
case FAVORITE -> getString(R.string.user_favorited);
case POLL -> getString(R.string.poll_ended);
case UPDATE -> getString(R.string.sk_post_edited);
case SIGN_UP -> getString(R.string.sk_signed_up);
case REPORT -> getString(R.string.sk_reported);
case REACTION, PLEROMA_EMOJI_REACTION ->
n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted);
};
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null;
if(n.status!=null){
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, Filter.FilterContext.NOTIFICATIONS);
int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); // | StatusDisplayItem.FLAG_NO_HEADER);
ArrayList<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags);
if(titleItem!=null)
items.add(0, titleItem);
return items;
}else if(titleItem!=null){
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this,
reportTarget != null ? reportTarget : n.account, n);
TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ?
new TextStatusDisplayItem(n.id, n.report.comment, this,
Status.ofFake(n.id, n.report.comment, n.createdAt), true) :
null;
return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card);
return Collections.singletonList(titleItem);
}else{
return Collections.emptyList();
}
}
@Override
protected void addAccountToKnown(Notification s){
if(!knownAccounts.containsKey(s.account.id))
@@ -144,52 +124,38 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
protected void doLoadData(int offset, int count){
AccountSessionManager.getInstance()
.getAccount(accountID).getCacheController()
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
@Override
public void onSuccess(CacheablePaginatedResponse<List<Notification>> result){
if (getActivity() == null) return;
if(refreshing)
relationships.clear();
public void onSuccess(PaginatedResponse<List<Notification>> result){
if(getActivity()==null)
return;
maxID=result.maxID;
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
Set<String> needRelationships=result.items.stream()
.filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id))
.map(ntf->ntf.account.id)
.collect(Collectors.toSet());
loadRelationships(needRelationships);
Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers;
if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){
E.post(new AllNotificationsSeenEvent());
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (isInstanceAkkoma()) {
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
reloadingFromCache=false;
if (getParentFragment() instanceof NotificationsFragment nf) {
nf.updateMarkAllReadButton();
}
}
});
}
@Override
protected void onRelationshipsLoaded(){
if(getActivity()==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
accountHolder.rebind();
protected void onShown(){
super.onShown();
if(!dataLoading){
if(onlyMentions){
refresh();
}else{
reloadingFromCache=true;
refresh();
}
}
}
@Override
protected void onShown(){
super.onShown();
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
// loadData();
protected void onHidden(){
super.onHidden();
resetUnreadBackground();
}
@Override
@@ -205,7 +171,50 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new InsetStatusItemDecoration(this));
if (onlyPosts) bannerHelper.maybeAddBanner(contentWrap);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint();
private Rect tmpRect=new Rect();
{
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
if (getParentFragment() instanceof NotificationsFragment nf) {
if(TextUtils.isEmpty(nf.unreadMarker))
return;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
String itemID=holder.getItemID();
if(ObjectIdComparator.INSTANCE.compare(itemID, nf.unreadMarker)>0){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
c.drawRect(tmpRect, paint);
}
}
}
}
}
}, 0);
}
@Override
protected List<View> getViewsForElevationEffect(){
if (getParentFragment() instanceof NotificationsFragment nf) {
ArrayList<View> views=new ArrayList<>(super.getViewsForElevationEffect());
views.add(nf.tabLayout);
return views;
} else {
return super.getViewsForElevationEffect();
}
}
@Override
public void onSaveInstanceState(Bundle outState){
super.onSaveInstanceState(outState);
outState.putBoolean("onlyMentions", onlyMentions);
outState.putBoolean("onlyPosts", onlyPosts);
}
private Notification getNotificationByID(String id){
@@ -238,6 +247,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==n.status.getContentStatus()){
@@ -252,6 +262,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
if (n.status == null) continue;
if(n.status.getContentStatus().id.equals(ev.id)){
n.status.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateNotification(n);
}
}
}
@@ -289,6 +300,40 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
@Override
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount());
}
void resetUnreadBackground(){
if (getParentFragment() instanceof NotificationsFragment nf) {
nf.unreadMarker=nf.realUnreadMarker;
list.invalidate();
}
}
@Override
public void onRefresh(){
super.onRefresh();
if (getParentFragment() instanceof NotificationsFragment nf) {
if (!onlyMentions && !onlyPosts) nf.markAsRead();
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
nf.unreadMarker=nf.realUnreadMarker=m;
nf.updateMarkAllReadButton();
});
}
resetUnreadBackground();
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
if (bannerHelper == null) return super.getAdapter();
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return base.path(isInstanceAkkoma()

View File

@@ -7,20 +7,21 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.Toast;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.TimelineDefinition;
import java.util.ArrayList;
import java.util.List;
public abstract class PinnableStatusListFragment extends StatusListFragment {
protected List<TimelineDefinition> pinnedTimelines;
protected List<TimelineDefinition> timelines;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
timelines=new ArrayList<>(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
}
@Override
@@ -30,7 +31,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
}
protected boolean isPinned() {
return pinnedTimelines.contains(makeTimelineDefinition());
return timelines.contains(makeTimelineDefinition());
}
protected void updatePinButton(MenuItem pin) {
@@ -57,11 +58,12 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
getToolbar().performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
TimelineDefinition def = makeTimelineDefinition();
boolean pinned = isPinned();
if (pinned) pinnedTimelines.remove(def);
else pinnedTimelines.add(def);
if (pinned) timelines.remove(def);
else timelines.add(def);
Toast.makeText(getContext(), pinned ? R.string.sk_unpinned_timeline : R.string.sk_pinned_timeline, Toast.LENGTH_SHORT).show();
GlobalUserPreferences.pinnedTimelines.put(accountID, pinnedTimelines);
GlobalUserPreferences.save();
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
prefs.timelines=new ArrayList<>(timelines);
prefs.save();
updatePinButton(pin);
}

View File

@@ -6,7 +6,7 @@ import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.parceler.Parcels;
@@ -41,8 +41,8 @@ public class PinnedPostsListFragment extends StatusListFragment{
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.ACCOUNT;
protected FilterContext getFilterContext() {
return FilterContext.ACCOUNT;
}
@Override

View File

@@ -13,6 +13,7 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import org.joinmastodon.android.R;
@@ -43,16 +44,15 @@ import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareFragment{
private static final int MAX_FIELDS=4;
static final int MAX_FIELDS=Integer.MAX_VALUE;
public UsableRecyclerView list;
private List<AccountField> fields=Collections.emptyList();
private AboutAdapter adapter;
private Paint dividerPaint=new Paint();
private boolean isInEditMode;
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
private RecyclerView.ViewHolder draggedViewHolder;
private ListImageLoaderWrapper imgLoader;
private boolean editDirty;
public void setFields(List<AccountField> fields){
this.fields=fields;
@@ -74,27 +74,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
list.setLayoutManager(new LinearLayoutManager(getActivity()));
imgLoader=new ListImageLoaderWrapper(getActivity(), list, new RecyclerViewDelegate(list), null);
list.setAdapter(adapter=new AboutAdapter());
int pad=V.dp(16);
list.setPadding(pad, pad, pad, pad);
list.setPadding(0, V.dp(16), 0, 0);
list.setClipToPadding(false);
dividerPaint.setStyle(Paint.Style.STROKE);
dividerPaint.setStrokeWidth(V.dp(1));
dividerPaint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorPollVoted));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void onDrawOver(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
for(int i=0;i<parent.getChildCount();i++){
View item=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(item);
int draggedPos=draggedViewHolder==null ? -1 : draggedViewHolder.getAbsoluteAdapterPosition();
if(pos<adapter.getItemCount()-1 && pos!=draggedPos && pos!=draggedPos-1){
float y=item.getY()+item.getHeight();
dividerPaint.setAlpha(Math.round(255*item.getAlpha()));
c.drawLine(item.getLeft(), y, item.getRight(), y, dividerPaint);
}
}
}
});
return list;
}
@@ -103,12 +84,17 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
fields=editableFields;
adapter.notifyDataSetChanged();
dragHelper.attachToRecyclerView(list);
editDirty=false;
}
public List<AccountField> getFields(){
return fields;
}
public boolean isEditDirty(){
return editDirty;
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
@@ -183,36 +169,25 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private abstract class BaseViewHolder extends BindableViewHolder<AccountField>{
protected ShapeDrawable background=new ShapeDrawable();
public BaseViewHolder(int layout){
super(getActivity(), layout, list);
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
itemView.setBackground(background);
}
@Override
public void onBind(AccountField item){
boolean first=getAbsoluteAdapterPosition()==0, last=getAbsoluteAdapterPosition()==adapter.getItemCount()-1;
float radius=V.dp(10);
float[] rad=new float[8];
if(first)
rad[0]=rad[1]=rad[2]=rad[3]=radius;
if(last)
rad[4]=rad[5]=rad[6]=rad[7]=radius;
background.setShape(new RoundRectShape(rad, null, null));
itemView.invalidateOutline();
}
}
private class AboutViewHolder extends BaseViewHolder implements ImageLoaderViewHolder{
private TextView title;
private LinkedTextView value;
private final TextView title;
private final LinkedTextView value;
// private final ImageView verifiedIcon;
public AboutViewHolder(){
super(R.layout.item_profile_about);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
// verifiedIcon=findViewById(R.id.verified_icon);
}
@Override
@@ -220,20 +195,7 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
super.onBind(item);
title.setText(item.parsedName);
value.setText(item.parsedValue);
if(item.verifiedAt!=null){
background.getPaint().setColor(UiUtils.isDarkTheme() ? 0xFF49595a : 0xFFd7e3da);
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
value.setTextColor(textColor);
value.setLinkTextColor(textColor);
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_24_regular, getActivity().getTheme()).mutate();
check.setTint(textColor);
value.setCompoundDrawablesRelativeWithIntrinsicBounds(null, null, check, null);
}else{
background.getPaint().setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorBackgroundLight));
value.setTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
value.setLinkTextColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorAccent));
value.setCompoundDrawables(null, null, null, null);
}
// verifiedIcon.setVisibility(item.verifiedAt!=null ? View.VISIBLE : View.GONE);
}
@Override
@@ -251,27 +213,38 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
private class EditableAboutViewHolder extends BaseViewHolder{
private EditText title;
private EditText value;
private final EditText title;
private final EditText value;
private boolean ignoreTextChange;
public EditableAboutViewHolder(){
super(R.layout.item_profile_about_editable);
super(R.layout.onboarding_profile_field);
title=findViewById(R.id.title);
value=findViewById(R.id.value);
value=findViewById(R.id.content);
findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
dragHelper.startDrag(this);
return true;
});
title.addTextChangedListener(new SimpleTextWatcher(e->item.name=e.toString()));
value.addTextChangedListener(new SimpleTextWatcher(e->item.value=e.toString()));
findViewById(R.id.remove_row_btn).setOnClickListener(this::onRemoveRowClick);
title.addTextChangedListener(new SimpleTextWatcher(e->{
item.name=e.toString();
if(!ignoreTextChange)
editDirty=true;
}));
value.addTextChangedListener(new SimpleTextWatcher(e->{
item.value=e.toString();
if(!ignoreTextChange)
editDirty=true;
}));
findViewById(R.id.delete).setOnClickListener(this::onRemoveRowClick);
}
@Override
public void onBind(AccountField item){
super.onBind(item);
ignoreTextChange=true;
title.setText(item.name);
value.setText(item.value);
ignoreTextChange=false;
}
private void onRemoveRowClick(View v){
@@ -323,8 +296,8 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
}
}
adapter.notifyItemMoved(fromPosition, toPosition);
((BindableViewHolder)viewHolder).rebind();
((BindableViewHolder)target).rebind();
((BindableViewHolder<?>)viewHolder).rebind();
((BindableViewHolder<?>)target).rebind();
return true;
}
@@ -339,7 +312,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG){
viewHolder.itemView.setTag(me.grishka.appkit.R.id.item_touch_helper_previous_elevation, viewHolder.itemView.getElevation()); // prevents the default behavior of changing elevation in onDraw()
viewHolder.itemView.animate().translationZ(V.dp(1)).setDuration(200).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=viewHolder;
}
}
@@ -347,7 +319,6 @@ public class ProfileAboutFragment extends Fragment implements WindowInsetsAwareF
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
super.clearView(recyclerView, viewHolder);
viewHolder.itemView.animate().translationZ(0).setDuration(100).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
draggedViewHolder=null;
}
@Override

View File

@@ -1,50 +0,0 @@
package org.joinmastodon.android.fragments;
import android.content.Context;
import android.os.Bundle;
import android.view.View;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
public abstract class RecyclerFragment<T> extends BaseRecyclerFragment<T> {
public RecyclerFragment(int perPage) {
super(perPage);
}
public RecyclerFragment(int layout, int perPage) {
super(layout, perPage);
}
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (refreshLayout != null) setRefreshLayoutColors(refreshLayout);
}
public static void setRefreshLayoutColors(SwipeRefreshLayout l) {
List<Integer> colors = new ArrayList<>(Arrays.asList(
R.color.primary_600,
R.color.red_primary_600,
R.color.green_primary_600,
R.color.blue_primary_600,
R.color.purple_600
));
int primary = UiUtils.getThemeColorRes(l.getContext(), R.attr.colorPrimary600);
if (!colors.contains(primary)) colors.add(0, primary);
int offset = colors.indexOf(primary);
int[] sorted = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
sorted[i] = colors.get((i + offset) % colors.size());
}
l.setColorSchemeResources(sorted);
}
}

View File

@@ -80,7 +80,7 @@ public class ScheduledStatusListFragment extends BaseStatusListFragment<Schedule
@Override
protected List<StatusDisplayItem> buildDisplayItems(ScheduledStatus s) {
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null);
return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, true, null);
}
@Override

View File

@@ -7,20 +7,27 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ProgressBar;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceChooserLoginFragment;
import org.joinmastodon.android.fragments.onboarding.InstanceRulesFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogDefaultInstance;
import org.joinmastodon.android.ui.InterpolatingMotionEffect;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
import org.parceler.Parcels;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import androidx.annotation.Nullable;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
@@ -37,11 +44,16 @@ public class SplashFragment extends AppKitFragment{
private View artContainer, blueFill, greenFill;
private InterpolatingMotionEffect motionEffect;
private View artClouds, artPlaneElephant, artRightHill, artLeftHill, artCenterHill;
private ProgressBarButton defaultServerButton;
private ProgressBar defaultServerProgress;
private String chosenDefaultServer=DEFAULT_SERVER;
private boolean loadingDefaultServer;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
motionEffect=new InterpolatingMotionEffect(MastodonApp.context);
loadAndChooseDefaultServer();
}
@Nullable
@@ -50,9 +62,14 @@ public class SplashFragment extends AppKitFragment{
contentView=(SizeListenerFrameLayout) inflater.inflate(R.layout.fragment_splash, container, false);
contentView.findViewById(R.id.btn_get_started).setOnClickListener(this::onButtonClick);
contentView.findViewById(R.id.btn_log_in).setOnClickListener(this::onButtonClick);
Button joinDefault=contentView.findViewById(R.id.btn_join_default_server);
joinDefault.setText(getString(R.string.join_default_server, DEFAULT_SERVER));
joinDefault.setOnClickListener(this::onJoinDefaultServerClick);
defaultServerButton=contentView.findViewById(R.id.btn_join_default_server);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
defaultServerButton.setOnClickListener(this::onJoinDefaultServerClick);
defaultServerProgress=contentView.findViewById(R.id.action_progress);
if(loadingDefaultServer){
defaultServerButton.setTextVisible(false);
defaultServerProgress.setVisibility(View.VISIBLE);
}
contentView.findViewById(R.id.btn_learn_more).setOnClickListener(this::onLearnMoreClick);
artClouds=contentView.findViewById(R.id.art_clouds);
@@ -96,12 +113,22 @@ public class SplashFragment extends AppKitFragment{
}
private void onJoinDefaultServerClick(View v){
if(loadingDefaultServer)
return;
new GetInstance()
.setCallback(new Callback<>(){
@Override
public void onSuccess(Instance result){
if(getActivity()==null)
return;
if(!result.registrations){
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.error)
.setMessage(R.string.instance_signup_closed)
.setPositiveButton(R.string.ok, null)
.show();
return;
}
Bundle args=new Bundle();
args.putParcelable("instance", Parcels.wrap(result));
Nav.go(getActivity(), InstanceRulesFragment.class, args);
@@ -115,7 +142,7 @@ public class SplashFragment extends AppKitFragment{
}
})
.wrapProgress(getActivity(), R.string.loading_instance, true)
.execNoAuth(DEFAULT_SERVER);
.execNoAuth(chosenDefaultServer);
}
private void onLearnMoreClick(View v){
@@ -168,4 +195,54 @@ public class SplashFragment extends AppKitFragment{
super.onHidden();
motionEffect.deactivate();
}
private void loadAndChooseDefaultServer(){
loadingDefaultServer=true;
new GetCatalogDefaultInstances()
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<CatalogDefaultInstance> result){
if(result.isEmpty()){
setChosenDefaultServer(DEFAULT_SERVER);
return;
}
float sum=0f;
for(CatalogDefaultInstance inst:result){
sum+=inst.weight;
}
if(sum<=0)
sum=1f;
for(CatalogDefaultInstance inst:result){
inst.weight/=sum;
}
float rand=ThreadLocalRandom.current().nextFloat();
float prev=0f;
for(CatalogDefaultInstance inst:result){
if(rand>=prev && rand<prev+inst.weight){
setChosenDefaultServer(inst.domain);
return;
}
prev+=inst.weight;
}
// Just in case something didn't add up
setChosenDefaultServer(result.get(result.size()-1).domain);
}
@Override
public void onError(ErrorResponse error){
setChosenDefaultServer(DEFAULT_SERVER);
}
})
.execNoAuth("");
}
private void setChosenDefaultServer(String domain){
chosenDefaultServer=domain;
loadingDefaultServer=false;
if(defaultServerButton!=null && getActivity()!=null){
defaultServerButton.setTextVisible(true);
defaultServerProgress.setVisibility(View.GONE);
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
}
}
}

View File

@@ -7,7 +7,7 @@ import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
@@ -57,7 +57,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, true, false, null, null);
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET);
int idx=data.indexOf(s);
if(idx>=0){
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
@@ -160,7 +160,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
}
@Override
protected Filter.FilterContext getFilterContext() {
protected FilterContext getFilterContext() {
return null;
}

View File

@@ -1,6 +1,5 @@
package org.joinmastodon.android.fragments;
import android.app.assist.AssistContent;
import android.content.res.Configuration;
import android.os.Bundle;
@@ -8,13 +7,14 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
@@ -35,10 +35,10 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
protected List<StatusDisplayItem> buildDisplayItems(Status s){
boolean addFooter = !GlobalUserPreferences.spectatorMode ||
(this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id));
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, false, addFooter, null, getFilterContext());
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), addFooter ? 0 : StatusDisplayItem.FLAG_NO_FOOTER);
}
protected abstract Filter.FilterContext getFilterContext();
protected abstract FilterContext getFilterContext();
@Override
protected void addAccountToKnown(Status s){
@@ -65,6 +65,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
Status status=getContentStatusByID(id);
if(status==null)
return;
Status parentStatus = getStatusByID(id);
if (parentStatus != status) {
status.spoilerRevealed = parentStatus.spoilerRevealed;
status.sensitiveRevealed = parentStatus.sensitiveRevealed;
}
status.filterRevealed = true;
Bundle args=new Bundle();
args.putString("account", accountID);
@@ -74,26 +79,26 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
Nav.go(getActivity(), ThreadFragment.class, args);
}
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusCreated(Status status){}
protected void onStatusUpdated(StatusUpdatedEvent ev){
protected void onStatusUpdated(Status status){
ArrayList<Status> statusesForDisplayItems=new ArrayList<>();
for(int i=0;i<data.size();i++){
Status s=data.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
if(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=status.clone();
statusesForDisplayItems.add(s);
}else if(s.id.equals(ev.status.id)){
data.set(i, ev.status);
statusesForDisplayItems.add(ev.status);
}else if(s.id.equals(status.id)){
data.set(i, status);
statusesForDisplayItems.add(status);
}
}
for(int i=0;i<preloadedData.size();i++){
Status s=preloadedData.get(i);
if(s.reblog!=null && s.reblog.id.equals(ev.status.id)){
s.reblog=ev.status;
}else if(s.id.equals(ev.status.id)){
preloadedData.set(i, ev.status);
if(s.reblog!=null && s.reblog.id.equals(status.id)){
s.reblog=status.clone();
}else if(s.id.equals(status.id)){
preloadedData.set(i, status);
}
}
@@ -211,6 +216,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status s:data){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof FooterStatusDisplayItem.Holder footer && footer.getItem().status==s.getContentStatus()){
@@ -224,6 +230,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status s:preloadedData){
if(s.getContentStatus().id.equals(ev.id)){
s.getContentStatus().update(ev);
AccountSessionManager.get(accountID).getCacheController().updateStatus(s);
}
}
}
@@ -242,12 +249,12 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
public void onStatusCreated(StatusCreatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
StatusListFragment.this.onStatusCreated(ev);
StatusListFragment.this.onStatusCreated(ev.status.clone());
}
@Subscribe
public void onStatusUpdated(StatusUpdatedEvent ev){
StatusListFragment.this.onStatusUpdated(ev);
StatusListFragment.this.onStatusUpdated(ev.status);
}
@Subscribe
@@ -257,7 +264,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
for(Status status:data){
Status contentStatus=status.getContentStatus();
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
updatePoll(status.id, status, ev.poll);
updatePoll(status.id, contentStatus, ev.poll);
}
}
}

View File

@@ -17,7 +17,7 @@ import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUpdatedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.model.StatusContext;
import org.joinmastodon.android.ui.BetterItemAnimator;
@@ -152,6 +152,26 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}).exec(accountID);
}
private void restoreStatusStates(List<Status> newData, Map<String, Status> oldData) {
for (Status s : newData) {
if (s == mainStatus) continue;
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.sensitiveRevealed = oldStatus.sensitiveRevealed;
s.filterRevealed = oldStatus.filterRevealed;
}
if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText)) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = mainStatus.spoilerRevealed;
}
}
}
}
protected void maybeApplyContext() {
if (!transitionFinished || result == null || getContext() == null) return;
Map<String, Status> oldData = null;
@@ -170,6 +190,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
result.descendants=filterStatuses(result.descendants);
result.ancestors=filterStatuses(result.ancestors);
restoreStatusStates(result.descendants, oldData);
restoreStatusStates(result.ancestors, oldData);
for (NeighborAncestryInfo i : mapNeighborhoodAncestry(mainStatus, result)) {
ancestryMap.put(i.status.id, i);
@@ -178,8 +200,10 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
if(footerProgress!=null)
footerProgress.setVisibility(View.GONE);
data.addAll(result.descendants);
int prevCount=displayItems.size();
onAppendItems(result.descendants);
int count=displayItems.size();
if(!refreshing)
adapter.notifyItemRangeInserted(prevCount, count-prevCount);
@@ -190,22 +214,6 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
count--;
}
for (Status s : data) {
Status oldStatus = oldData == null ? null : oldData.get(s.id);
// restore previous spoiler/filter revealed states when refreshing
if (oldStatus != null) {
s.spoilerRevealed = oldStatus.spoilerRevealed;
s.filterRevealed = oldStatus.filterRevealed;
} else if (GlobalUserPreferences.autoRevealEqualSpoilers != AutoRevealMode.NEVER &&
s.spoilerText != null &&
s.spoilerText.equals(mainStatus.spoilerText) &&
mainStatus.spoilerRevealed) {
if (GlobalUserPreferences.autoRevealEqualSpoilers == AutoRevealMode.DISCUSSIONS || Objects.equals(mainStatus.account.id, s.account.id)) {
s.spoilerRevealed = true;
}
}
}
dataLoaded();
if(refreshing){
refreshDone();
@@ -222,13 +230,13 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
result = null;
}
protected Object maybeApplyMainStatus() {
if (updatedStatus == null || !contextInitiallyRendered) return null;
// restore revealed states for main status because it gets updated after doLoadData
updatedStatus.filterRevealed = mainStatus.filterRevealed;
updatedStatus.spoilerRevealed = mainStatus.spoilerRevealed;
updatedStatus.sensitiveRevealed = mainStatus.sensitiveRevealed;
// returning fired event object to facilitate testing
Object event;
@@ -352,9 +360,9 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
});
}
protected void onStatusCreated(StatusCreatedEvent ev){
if (ev.status.inReplyToId == null) return;
Status repliedToStatus = getStatusByID(ev.status.inReplyToId);
protected void onStatusCreated(Status status){
if (status.inReplyToId == null) return;
Status repliedToStatus = getStatusByID(status.inReplyToId);
if (repliedToStatus == null) return;
NeighborAncestryInfo ancestry = ancestryMap.get(repliedToStatus.id);
@@ -400,12 +408,12 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
}
// update replied-to status' ancestry
if (ancestry != null) ancestry.descendantNeighbor = ev.status;
if (ancestry != null) ancestry.descendantNeighbor = status;
// add ancestry for newly created status before building its display items
ancestryMap.put(ev.status.id, new NeighborAncestryInfo(ev.status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(ev.status));
data.add(nextDataIndex, ev.status);
ancestryMap.put(status.id, new NeighborAncestryInfo(status, null, repliedToStatus));
displayItems.addAll(nextDisplayItemsIndex, buildDisplayItems(status));
data.add(nextDataIndex, status);
adapter.notifyDataSetChanged();
}
@@ -426,8 +434,8 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.THREAD;
protected FilterContext getFilterContext() {
return FilterContext.THREAD;
}
@Override

View File

@@ -1,44 +1,22 @@
package org.joinmastodon.android.fragments.account_list;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.app.assist.AssistContent;
import android.content.Intent;
import android.content.res.Configuration;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toolbar;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.fragments.ListsFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.text.HtmlParser;
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.HashMap;
@@ -48,21 +26,16 @@ import java.util.stream.Collectors;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
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.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccountListFragment.AccountItem> implements ProvidesAssistContent.ProvidesWebUri {
public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<AccountViewModel> implements ProvidesAssistContent.ProvidesWebUri {
protected HashMap<String, Relationship> relationships=new HashMap<>();
protected String accountID;
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
@@ -71,6 +44,10 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super(40);
}
public BaseAccountListFragment(int layout, int perPage){
super(layout, perPage);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
@@ -78,7 +55,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
}
@Override
protected void onDataLoaded(List<AccountItem> d, boolean more){
protected void onDataLoaded(List<AccountViewModel> d, boolean more){
if(refreshing){
relationships.clear();
}
@@ -95,7 +72,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
super.onRefresh();
}
protected void loadRelationships(List<AccountItem> accounts){
protected void loadRelationships(List<AccountViewModel> accounts){
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
GetAccountRelationships req=new GetAccountRelationships(ids);
relationshipsRequests.add(req);
@@ -132,10 +109,7 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
// list.setPadding(0, V.dp(16), 0, V.dp(16));
list.setClipToPadding(false);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1,
Math.round(16f + 56f * getResources().getConfiguration().fontScale), 16));
updateToolbar();
}
@@ -168,6 +142,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, V.dp(16), 0, V.dp(16)+insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, V.dp(16), 0, V.dp(16));
@@ -185,6 +161,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
assistContent.setWebUri(getWebUri(getSession().getInstanceUri().buildUpon()));
}
protected void onConfigureViewHolder(AccountViewHolder holder){}
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
public AccountsAdapter(){
super(imgLoader);
@@ -193,7 +171,9 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new AccountViewHolder();
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
onConfigureViewHolder(holder);
return holder;
}
@Override
@@ -214,225 +194,8 @@ public abstract class BaseAccountListFragment extends RecyclerFragment<BaseAccou
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
AccountItem item=data.get(position);
AccountViewModel item=data.get(position);
return image==0 ? item.avaRequest : item.emojiHelper.getImageRequest(image-1);
}
}
protected class AccountViewHolder extends BindableViewHolder<AccountItem> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable, UsableRecyclerView.LongClickable{
private final TextView name, username;
private final ImageView avatar;
private final Button button;
private final PopupMenu contextMenu;
private final View menuAnchor;
public AccountViewHolder(){
super(getActivity(), R.layout.item_account_list, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
avatar=findViewById(R.id.avatar);
button=findViewById(R.id.button);
menuAnchor=findViewById(R.id.menu_anchor);
avatar.setOutlineProvider(OutlineProviders.roundedRect(12));
avatar.setClipToOutline(true);
button.setOnClickListener(this::onButtonClick);
contextMenu=new PopupMenu(getActivity(), menuAnchor);
contextMenu.inflate(R.menu.profile);
contextMenu.setOnMenuItemClickListener(this::onContextMenuItemSelected);
UiUtils.enablePopupMenuIcons(getActivity(), contextMenu);
}
@SuppressLint("SetTextI18n")
@Override
public void onBind(AccountItem item){
name.setText(item.parsedName);
username.setText("@"+ (item.account.isRemote
? item.account.getFullyQualifiedName()
: item.account.acct));
bindRelationship();
}
public void bindRelationship(){
Relationship rel=relationships.get(item.account.id);
if(rel==null || item.account.isRemote || AccountSessionManager.getInstance().isSelf(accountID, item.account)){
button.setVisibility(View.GONE);
}else{
button.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(rel, button);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
if (item.account.isRemote) args.putParcelable("remoteAccount", Parcels.wrap(item.account));
else args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
@Override
public boolean onLongClick(){
return false;
}
@Override
public boolean onLongClick(float x, float y){
Relationship relationship=relationships.get(item.account.id);
if(relationship==null)
return false;
Menu menu=contextMenu.getMenu();
Account account=item.account;
menu.findItem(R.id.share).setTitle(getString(R.string.share_user, account.getShortUsername()));
MenuItem mute = menu.findItem(R.id.mute);
mute.setTitle(getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
mute.setIcon(relationship.muting ? R.drawable.ic_fluent_speaker_0_24_regular : R.drawable.ic_fluent_speaker_off_24_regular);
UiUtils.insetPopupMenuIcon(getContext(), mute);
menu.findItem(R.id.block).setTitle(getString(relationship.blocking ? R.string.unblock_user : R.string.block_user, account.getShortUsername()));
menu.findItem(R.id.report).setTitle(getString(R.string.report_user, account.getShortUsername()));
menu.findItem(R.id.soft_block).setVisible(relationship.followedBy && !relationship.following);
MenuItem hideBoosts=menu.findItem(R.id.hide_boosts);
MenuItem manageUserLists=menu.findItem(R.id.manage_user_lists);
if(relationship.following){
hideBoosts.setTitle(getString(relationship.showingReblogs ? R.string.hide_boosts_from_user : R.string.show_boosts_from_user, account.getShortUsername()));
hideBoosts.setIcon(relationship.showingReblogs ? R.drawable.ic_fluent_arrow_repeat_all_off_24_regular : R.drawable.ic_fluent_arrow_repeat_all_24_regular);
hideBoosts.setVisible(true);
UiUtils.insetPopupMenuIcon(getContext(), hideBoosts);
manageUserLists.setTitle(getString(R.string.sk_lists_with_user, account.getShortUsername()));
manageUserLists.setVisible(true);
}else{
hideBoosts.setVisible(false);
manageUserLists.setVisible(false);
}
menu.findItem(R.id.block_domain).setVisible(false);
menuAnchor.setTranslationX(x);
menuAnchor.setTranslationY(y);
contextMenu.show();
return true;
}
private void onButtonClick(View v){
ProgressDialog progress=new ProgressDialog(getActivity());
progress.setMessage(getString(R.string.loading));
progress.setCancelable(false);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationships.get(item.account.id), button, progressShown->{
itemView.setHasTransientState(progressShown);
if(progressShown)
progress.show();
else
progress.dismiss();
}, result->{
relationships.put(item.account.id, result);
bindRelationship();
});
}
private boolean onContextMenuItemSelected(MenuItem item){
Relationship relationship=relationships.get(this.item.account.id);
if(relationship==null)
return false;
Account account=this.item.account;
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.mute){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, account, relationship.muting, this::updateRelationship);
}else if(id==R.id.block){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, account, relationship.blocking, this::updateRelationship);
}else if(id==R.id.soft_block){
UiUtils.confirmSoftBlockUser(getActivity(), accountID, account, this::updateRelationship);
}else if(id==R.id.report){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(account));
Nav.go(getActivity(), ReportReasonChoiceFragment.class, args);
}else if(id==R.id.open_in_browser){
UiUtils.launchWebBrowser(getActivity(), account.url);
}else if(id==R.id.block_domain){
UiUtils.confirmToggleBlockDomain(getActivity(), accountID, account.getDomain(), relationship.domainBlocking, ()->{
relationship.domainBlocking=!relationship.domainBlocking;
bindRelationship();
});
}else if(id==R.id.hide_boosts){
new SetAccountFollowed(account.id, true, !relationship.showingReblogs, relationship.notifying)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
relationships.put(AccountViewHolder.this.item.account.id, result);
if (getActivity() == null) return;
bindRelationship();
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.loading, false)
.exec(accountID);
}else if(id==R.id.manage_user_lists){
final Bundle args=new Bundle();
args.putString("account", accountID);
args.putString("profileAccount", account.id);
args.putString("profileDisplayUsername", account.getDisplayUsername());
Nav.go(getActivity(), ListsFragment.class, args);
}
return true;
}
private void updateRelationship(Relationship r){
relationships.put(item.account.id, r);
bindRelationship();
}
}
protected static class AccountItem{
public final Account account;
public final ImageLoaderRequest avaRequest;
public final CustomEmojiHelper emojiHelper;
public final CharSequence parsedName;
public AccountItem(Account account){
this.account=account;
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
emojiHelper=new CustomEmojiHelper();
emojiHelper.setText(parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis));
}
@Override
public boolean equals(@Nullable Object obj) {
return obj instanceof AccountItem i && i.account.url.equals(account.url);
}
}
}

View File

@@ -0,0 +1,105 @@
package org.joinmastodon.android.fragments.account_list;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.parceler.Parcels;
import java.util.stream.Collectors;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
private String currentQuery;
private boolean resultDelivered;
private SearchViewHelper searchViewHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRefreshEnabled(false);
setEmptyText("");
dataLoaded();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_hint));
searchViewHelper.setListeners(this::onQueryChanged, null);
searchViewHelper.addDivider(contentView);
super.onViewCreated(view, savedInstanceState);
view.setBackgroundResource(R.drawable.bg_m3_surface3);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
setStatusBarColor(color);
setNavigationBarColor(color);
}
@Override
protected void doLoadData(int offset, int count){
refreshing=true;
currentRequest=new GetSearchResults(currentQuery, GetSearchResults.Type.ACCOUNTS, false)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.no_search_results);
onDataLoaded(result.accounts.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setOnClickListener(this::onItemClick);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
}
private void onItemClick(AccountViewHolder holder){
if(resultDelivered)
return;
resultDelivered=true;
Bundle res=new Bundle();
res.putParcelable("selectedAccount", Parcels.wrap(holder.getItem().account));
setResult(true, res);
Nav.finish(this, false);
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
if(!TextUtils.isEmpty(currentQuery))
loadData();
}
@Override
public Uri getWebUri(Uri.Builder base) {
return null;
}
}

View File

@@ -9,6 +9,7 @@ import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import java.util.Collection;
import java.util.Collections;
@@ -125,17 +126,17 @@ public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFra
@Override
public void onSuccess(HeaderPaginationList<Account> result){
boolean justRefreshed = !doneWithHomeInstance && offset == 0;
Collection<AccountItem> d = justRefreshed ? List.of() : data;
Collection<AccountViewModel> d = justRefreshed ? List.of() : data;
if(result.nextPageUri!=null)
nextMaxID=result.nextPageUri.getQueryParameter("max_id");
else
nextMaxID=null;
if (getActivity() == null) return;
List<AccountItem> items = result.stream()
List<AccountViewModel> items = result.stream()
.filter(a -> d.size() > 1000 || d.stream()
.noneMatch(i -> i.account.url.equals(a.url)))
.map(AccountItem::new)
.map(a->new AccountViewModel(a, accountID))
.collect(Collectors.toList());
boolean hasMore = nextMaxID != null;

View File

@@ -2,12 +2,12 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import androidx.recyclerview.widget.RecyclerView;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@@ -16,20 +16,27 @@ import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
private DiscoverInfoBannerHelper bannerHelper;
private String maxID;
@Override
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE, accountID);
}
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
@@ -43,15 +50,17 @@ public class BubbleTimelineFragment extends StatusListFragment {
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
@Override
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override

View File

@@ -17,8 +17,8 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowSuggestion;
@@ -51,7 +51,7 @@ import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
public class DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
@@ -242,7 +242,7 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount));
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount)));
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount)));
postsLabel.setText(getResources().getQuantityString(R.plurals.x_posts, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
@@ -252,7 +252,7 @@ public class DiscoverAccountsFragment extends RecyclerFragment<DiscoverAccountsF
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButton(relationship, actionButton);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
}
}

View File

@@ -1,64 +1,58 @@
package org.joinmastodon.android.fragments.discover;
import android.app.Fragment;
import android.app.assist.AssistContent;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.ui.OutlineProviders;
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 org.joinmastodon.android.utils.ProvidesAssistContent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.Nav;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop, ProvidesAssistContent {
public class DiscoverFragment extends AppKitFragment implements ScrollableToTop, OnBackPressedListener, IsOnTop {
private static final int QUERY_RESULT=937;
private TabLayout tabLayout;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private TabLayoutMediator tabLayoutMediator;
private EditText searchEdit;
private boolean searchActive;
private FrameLayout searchView;
private ImageButton searchBack, searchClear;
private ProgressBar searchProgress;
private ImageButton searchBack;
private TextView searchText;
private View tabsDivider;
private DiscoverPostsFragment postsFragment;
private DiscoverHashtagsFragment hashtagsFragment;
private TrendingHashtagsFragment hashtagsFragment;
private DiscoverNewsFragment newsFragment;
private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
private String currentQuery;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -92,12 +86,10 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabViews[i]=tabView;
}
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabLayout.setTabTextSize(V.dp(14));
UiUtils.reduceSwipeSensitivity(pager);
pager.setOffscreenPageLimit(4);
pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe);
pager.setAdapter(new DiscoverPagerAdapter());
pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){
@Override
@@ -120,7 +112,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
postsFragment=new DiscoverPostsFragment();
postsFragment.setArguments(args);
hashtagsFragment=new DiscoverHashtagsFragment();
hashtagsFragment=new TrendingHashtagsFragment();
hashtagsFragment.setArguments(args);
newsFragment=new DiscoverNewsFragment();
@@ -129,10 +121,9 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
accountsFragment=new DiscoverAccountsFragment();
accountsFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment)
.commit();
@@ -148,7 +139,6 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
case 3 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
}
});
tabLayoutMediator.attach();
@@ -165,62 +155,52 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
searchEdit=view.findViewById(R.id.search_edit);
searchEdit.setOnFocusChangeListener(this::onSearchEditFocusChanged);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
searchEdit.addTextChangedListener(new TextWatcher(){
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.VISIBLE);
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count){
searchEdit.removeCallbacks(searchDebouncer);
searchEdit.postDelayed(searchDebouncer, 300);
}
@Override
public void afterTextChanged(Editable s){
if(s.length()==0){
V.setVisibilityAnimated(searchClear, View.INVISIBLE);
}
}
});
searchView=view.findViewById(R.id.search_fragment);
if(searchFragment==null){
searchFragment=new SearchFragment();
Bundle args=new Bundle();
args.putString("account", accountID);
searchFragment.setArguments(args);
searchFragment.setProgressVisibilityListener(this::onSearchProgressVisibilityChanged);
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
}
searchBack=view.findViewById(R.id.search_back);
searchClear=view.findViewById(R.id.search_clear);
searchProgress=view.findViewById(R.id.search_progress);
searchBack.setEnabled(searchActive);
searchText=view.findViewById(R.id.search_text);
searchBack.setImportantForAccessibility(searchActive ? View.IMPORTANT_FOR_ACCESSIBILITY_YES : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
searchBack.setOnClickListener(v->exitSearch());
searchBack.setOnClickListener(v->{
if(searchActive) exitSearch(); else openSearch();
});
if(searchActive){
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
searchView.setVisibility(View.VISIBLE);
}
searchClear.setOnClickListener(v->{
searchEdit.setText("");
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
});
View searchWrap=view.findViewById(R.id.search_wrap);
searchWrap.setOutlineProvider(OutlineProviders.roundedRect(28));
searchWrap.setClipToOutline(true);
searchText.setOnClickListener(v->openSearch());
tabsDivider=view.findViewById(R.id.tabs_divider);
return view;
}
@Override
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void openSearch() {
Bundle args=new Bundle();
args.putString("account", accountID);
if(!TextUtils.isEmpty(currentQuery)){
args.putString("query", currentQuery);
}
Nav.goForResult(getActivity(), SearchQueryFragment.class, args, QUERY_RESULT, DiscoverFragment.this);
}
@Override
public void scrollToTop(){
if(!searchActive){
@@ -230,30 +210,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
}
@Override
public boolean isOnTop() {
return searchActive ? searchFragment.isOnTop()
: ((IsOnTop)getFragmentForPage(pager.getCurrentItem())).isOnTop();
}
public void onSelect() {
if (isOnTop()) selectSearch();
else scrollToTop();
}
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
}
public void loadData(){
if(hashtagsFragment!=null && !hashtagsFragment.loaded && !hashtagsFragment.dataLoading)
hashtagsFragment.loadData();
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading)
postsFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
if(!searchActive && hasFocus){
private void enterSearch(){
if(!searchActive){
searchActive=true;
pager.setVisibility(View.GONE);
tabLayout.setVisibility(View.GONE);
@@ -261,28 +224,23 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setImageResource(R.drawable.ic_fluent_arrow_left_24_regular);
searchBack.setEnabled(true);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
tabsDivider.setVisibility(View.GONE);
}
}
private void exitSearch(){
if(!searchActive)
return;
searchActive=false;
pager.setVisibility(View.VISIBLE);
tabLayout.setVisibility(View.VISIBLE);
searchView.setVisibility(View.GONE);
searchEdit.clearFocus();
searchEdit.setText("");
searchText.setText(R.string.search_mastodon);
searchBack.setImageResource(R.drawable.ic_fluent_search_24_regular);
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
if (getArguments().getBoolean("disableDiscover"))
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
tabsDivider.setVisibility(View.VISIBLE);
currentQuery=null;
}
private Fragment getFragmentForPage(int page){
@@ -304,30 +262,20 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
return false;
}
private void onSearchChangedDebounced(){
searchFragment.setQuery(searchEdit.getText().toString());
}
private boolean onSearchEnterPressed(TextView v, int actionId, KeyEvent event){
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
return true;
searchEdit.removeCallbacks(searchDebouncer);
onSearchChangedDebounced();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
return true;
}
private void onSearchProgressVisibilityChanged(boolean visible){
V.setVisibilityAnimated(searchProgress, visible ? View.VISIBLE : View.INVISIBLE);
if(searchEdit.length()>0)
V.setVisibilityAnimated(searchClear, visible ? View.INVISIBLE : View.VISIBLE);
}
@Override
public void onProvideAssistContent(AssistContent assistContent) {
callFragmentToProvideAssistContent(searchActive
? searchFragment
: getFragmentForPage(pager.getCurrentItem()), assistContent);
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(reqCode==QUERY_RESULT && success){
enterSearch();
currentQuery=result.getString("query");
SearchResult.Type type;
if(result.containsKey("filter")){
type=SearchResult.Type.values()[result.getInt("filter")];
}else{
type=null;
}
searchFragment.setQuery(currentQuery, type);
searchText.setText(currentQuery);
}
}
private class DiscoverPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{

View File

@@ -1,7 +1,7 @@
package org.joinmastodon.android.fragments.discover;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
@@ -11,36 +11,45 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingLinks;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Card;
import org.joinmastodon.android.model.viewmodel.CardViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.drawables.BlurhashCrossfadeDrawable;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.ListImageLoaderAdapter;
import me.grishka.appkit.imageloader.ListImageLoaderWrapper;
import me.grishka.appkit.imageloader.RecyclerViewDelegate;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverNewsFragment extends RecyclerFragment<Card> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
public class DiscoverNewsFragment extends BaseRecyclerFragment<CardViewModel> implements ScrollableToTop{
private String accountID;
private List<ImageLoaderRequest> imageRequests=Collections.emptyList();
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS);
private DiscoverInfoBannerHelper bannerHelper;
private MergeRecyclerAdapter mergeAdapter;
private UsableRecyclerView cardsList;
private ArrayList<CardViewModel> top3=new ArrayList<>();
private CardLinksAdapter cardsAdapter;
public DiscoverNewsFragment(){
super(10);
@@ -50,6 +59,7 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_LINKS, accountID);
}
@Override
@@ -58,11 +68,14 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Card> result){
imageRequests=result.stream()
.map(card->TextUtils.isEmpty(card.image) ? null : new UrlImageLoaderRequest(card.image, V.dp(150), V.dp(150)))
.collect(Collectors.toList());
if (getActivity() == null) return;
onDataLoaded(result, false);
top3.clear();
top3.addAll(result.subList(0, Math.min(3, result.size())).stream().map(card->new CardViewModel(card, 280, 140)).collect(Collectors.toList()));
cardsAdapter.notifyDataSetChanged();
onDataLoaded(result.subList(top3.size(), result.size()).stream()
.map(card->new CardViewModel(card, 56, 56))
.collect(Collectors.toList()), false);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
@@ -70,14 +83,27 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
@Override
protected RecyclerView.Adapter getAdapter(){
return new LinksAdapter();
}
cardsList=new UsableRecyclerView(getActivity());
cardsList.setLayoutManager(new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false));
ListImageLoaderWrapper cardsImageLoader=new ListImageLoaderWrapper(getActivity(), cardsList, new RecyclerViewDelegate(cardsList), this);
cardsList.setAdapter(cardsAdapter=new CardLinksAdapter(cardsImageLoader, top3));
cardsList.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(256)));
cardsList.setPadding(V.dp(16), V.dp(8), 0, 0);
cardsList.setClipToPadding(false);
cardsList.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
outRect.right=V.dp(16);
}
});
cardsList.setSelector(R.drawable.bg_rect_12dp_ripple);
cardsList.setDrawSelectorOnTop(true);
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 0, 0));
bannerHelper.maybeAddBanner(contentWrap);
mergeAdapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, mergeAdapter);
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(cardsList));
mergeAdapter.addAdapter(new LinksAdapter(imgLoader, data));
return mergeAdapter;
}
@Override
@@ -85,29 +111,17 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
private class LinksAdapter extends UsableRecyclerView.Adapter<BaseLinkViewHolder> implements ImageLoaderRecyclerAdapter{
private final List<CardViewModel> data;
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/links").build();
}
private class LinksAdapter extends UsableRecyclerView.Adapter<LinkViewHolder> implements ImageLoaderRecyclerAdapter{
public LinksAdapter(){
public LinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader);
this.data=data;
}
@NonNull
@Override
public LinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkViewHolder();
}
@@ -117,46 +131,51 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
}
@Override
public void onBindViewHolder(LinkViewHolder holder, int position){
holder.bind(data.get(position));
public void onBindViewHolder(BaseLinkViewHolder holder, int position){
holder.bind(data.get(position).card);
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return imageRequests.get(position)==null ? 0 : 1;
return data.get(position).imageRequest==null ? 0 : 1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
return imageRequests.get(position);
return data.get(position).imageRequest;
}
}
private class LinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
private final TextView name, title, subtitle;
private final ImageView photo;
private class CardLinksAdapter extends LinksAdapter{
public CardLinksAdapter(ListImageLoaderWrapper imgLoader, List<CardViewModel> data){
super(imgLoader, data);
}
@NonNull
@Override
public BaseLinkViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new LinkCardViewHolder();
}
}
private class BaseLinkViewHolder extends BindableViewHolder<Card> implements UsableRecyclerView.Clickable, ImageLoaderViewHolder{
protected final TextView name, title;
protected final ImageView photo;
private BlurhashCrossfadeDrawable crossfadeDrawable=new BlurhashCrossfadeDrawable();
private boolean didClear;
public LinkViewHolder(){
super(getActivity(), R.layout.item_trending_link, list);
public BaseLinkViewHolder(int layout){
super(getActivity(), layout, list);
name=findViewById(R.id.name);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
photo=findViewById(R.id.photo);
photo.setOutlineProvider(OutlineProviders.roundedRect(2));
photo.setClipToOutline(true);
}
@Override
public void onBind(Card item){
name.setText(item.providerName);
title.setText(item.title);
int num=item.history.get(0).uses;
if(item.history.size()>1)
num+=item.history.get(1).uses;
subtitle.setText(getResources().getQuantityString(R.plurals.discussed_x_times, num, num));
crossfadeDrawable.setSize(item.width, item.height);
crossfadeDrawable.setBlurhashDrawable(item.blurhashPlaceholder);
crossfadeDrawable.setCrossfadeAlpha(0f);
@@ -183,4 +202,20 @@ public class DiscoverNewsFragment extends RecyclerFragment<Card> implements Scro
UiUtils.launchWebBrowser(getActivity(), item.url);
}
}
private class LinkViewHolder extends BaseLinkViewHolder{
public LinkViewHolder(){
super(R.layout.item_trending_link);
photo.setOutlineProvider(OutlineProviders.roundedRect(12));
photo.setClipToOutline(true);
}
}
private class LinkCardViewHolder extends BaseLinkViewHolder{
public LinkCardViewHolder(){
super(R.layout.item_trending_link_card);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
}
}
}

View File

@@ -2,22 +2,27 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class DiscoverPostsFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS);
public class DiscoverPostsFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
}
@Override
protected void doLoadData(int offset, int count){
@@ -25,26 +30,27 @@ public class DiscoverPostsFragment extends StatusListFragment {
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
bannerHelper.onBannerBecameVisible();
}
}).exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
public Uri getWebUri(Uri.Builder base){
return isInstanceAkkoma() ? null : base.path("/explore/posts").build();
}
}

View File

@@ -2,55 +2,59 @@ package org.joinmastodon.android.fragments.discover;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class FederatedTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
public class FederatedTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE, accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override

View File

@@ -7,54 +7,59 @@ import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class LocalTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper;
public class LocalTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID);
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count)
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
boolean empty=result.isEmpty();
AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC);
onDataLoaded(result, !empty);
bannerHelper.onBannerBecameVisible();
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
bannerHelper.maybeAddBanner(list, adapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
protected FilterContext getFilterContext() {
return FilterContext.PUBLIC;
}
@Override
public Uri getWebUri(Uri.Builder base) {
public Uri getWebUri(Uri.Builder base){
return base.path(isInstanceAkkoma() ? "/main/public" : "/public/local").build();
}
}

View File

@@ -4,7 +4,6 @@ import android.app.Activity;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
@@ -15,7 +14,7 @@ import org.joinmastodon.android.fragments.BaseStatusListFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ThreadFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
@@ -24,7 +23,6 @@ import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.tabs.TabLayout;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
@@ -35,24 +33,18 @@ import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SearchFragment extends BaseStatusListFragment<SearchResult> {
public class SearchFragment extends BaseStatusListFragment<SearchResult>{
private String currentQuery;
private List<StatusDisplayItem> prevDisplayItems;
private EnumSet<SearchResult.Type> currentFilter=EnumSet.allOf(SearchResult.Type.class);
private List<SearchResult> unfilteredResults=Collections.emptyList();
private HideableSingleViewRecyclerAdapter headerAdapter;
private ProgressVisibilityListener progressVisibilityListener;
private InputMethodManager imm;
private TabLayout tabLayout;
public SearchFragment(){
setLayout(R.layout.fragment_search);
}
@@ -62,8 +54,8 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
super.onCreate(savedInstanceState);
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
setEmptyText(R.string.no_search_results);
loadData();
resetEmptyText();
}
@Override
@@ -72,16 +64,12 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
imm=activity.getSystemService(InputMethodManager.class);
}
private void resetEmptyText() {
setEmptyText(R.string.sk_recent_searches_placeholder);
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(SearchResult s){
return switch(s.type){
case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account));
case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag));
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, false, true, null, Filter.FilterContext.PUBLIC);
case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, FilterContext.PUBLIC, 0);
};
}
@@ -125,57 +113,52 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
@Override
protected void doLoadData(int offset, int count){
if (getActivity() == null) return;
resetEmptyText();
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(sr->{
if(getActivity()==null)
return;
unfilteredResults=sr;
prevDisplayItems=new ArrayList<>(displayItems);
onDataLoaded(sr, false);
});
GetSearchResults.Type type;
if(currentFilter.size()==1){
type=switch(currentFilter.iterator().next()){
case ACCOUNT -> GetSearchResults.Type.ACCOUNTS;
case HASHTAG -> GetSearchResults.Type.HASHTAGS;
case STATUS -> GetSearchResults.Type.STATUSES;
};
}else{
setEmptyText(R.string.sk_searching);
progressVisibilityListener.onProgressVisibilityChanged(true);
currentRequest=new GetSearchResults(currentQuery, null, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
setEmptyText(R.string.sk_no_results);
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
if (getActivity() == null) return;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
resetEmptyText();
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
})
.exec(accountID);
type=null;
}
if(currentQuery==null){
dataLoaded();
return;
}
currentRequest=new GetSearchResults(currentQuery, type, true)
.setCallback(new Callback<>(){
@Override
public void onSuccess(SearchResults result){
ArrayList<SearchResult> results=new ArrayList<>();
if(result.accounts!=null){
for(Account acc:result.accounts)
results.add(new SearchResult(acc));
}
if(result.hashtags!=null){
for(Hashtag tag:result.hashtags)
results.add(new SearchResult(tag));
}
if(result.statuses!=null){
for(Status status:result.statuses)
results.add(new SearchResult(status));
}
prevDisplayItems=new ArrayList<>(displayItems);
unfilteredResults=results;
onDataLoaded(filterSearchResults(results), false);
}
@Override
public void onError(ErrorResponse error){
currentRequest=null;
Activity a=getActivity();
if(a==null)
return;
error.showToast(a);
}
})
.exec(accountID);
}
@Override
@@ -185,86 +168,30 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
return;
}
UiUtils.updateList(prevDisplayItems, displayItems, list, adapter, (i1, i2)->i1.parentID.equals(i2.parentID) && i1.index==i2.index && i1.getType()==i2.getType());
boolean recent=isInRecentMode() && !displayItems.isEmpty();
if(recent!=headerAdapter.isVisible())
headerAdapter.setVisible(recent);
imgLoader.forceUpdateImages();
prevDisplayItems=null;
}
@Override
protected void onDataLoaded(List<SearchResult> d, boolean more){
super.onDataLoaded(d, more);
if(progressVisibilityListener!=null)
progressVisibilityListener.onProgressVisibilityChanged(false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
tabLayout=view.findViewById(R.id.tabbar);
tabLayout.setTabTextSize(V.dp(16));
tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_all));
tabLayout.addTab(tabLayout.newTab().setText(R.string.search_people));
tabLayout.addTab(tabLayout.newTab().setText(R.string.hashtags));
tabLayout.addTab(tabLayout.newTab().setText(R.string.posts));
for(int i=0;i<tabLayout.getTabCount();i++){
tabLayout.getTabAt(i).view.textView.setAllCaps(true);
}
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener(){
@Override
public void onTabSelected(TabLayout.Tab tab){
setFilter(switch(tab.getPosition()){
case 0 -> EnumSet.allOf(SearchResult.Type.class);
case 1 -> EnumSet.of(SearchResult.Type.ACCOUNT);
case 2 -> EnumSet.of(SearchResult.Type.HASHTAG);
case 3 -> EnumSet.of(SearchResult.Type.STATUS);
default -> throw new IllegalStateException("Unexpected value: "+tab.getPosition());
});
}
@Override
public void onTabUnselected(TabLayout.Tab tab){
}
@Override
public void onTabReselected(TabLayout.Tab tab){
scrollToTop();
}
});
}
@Override
protected RecyclerView.Adapter getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.item_recent_searches_header, list, false);
header.findViewById(R.id.clear).setOnClickListener(this::onClearRecentClick);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(headerAdapter=new HideableSingleViewRecyclerAdapter(header));
adapter.addAdapter(super.getAdapter());
headerAdapter.setVisible(isInRecentMode());
return adapter;
}
public void setQuery(String q){
if(Objects.equals(q, currentQuery) || q.isBlank())
public void setQuery(String q, SearchResult.Type filter){
if(q.isBlank())
return;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
currentQuery=q;
if(filter==null)
currentFilter=EnumSet.allOf(SearchResult.Type.class);
else
currentFilter=EnumSet.of(filter);
refreshing=true;
doLoadData(0, 0);
loadData();
}
private void setFilter(EnumSet<SearchResult.Type> filter){
if(filter.equals(currentFilter))
return;
currentFilter=filter;
if(isInRecentMode())
return;
// This can be optimized by not rebuilding display items every time filter is changed, but I'm too lazy
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
@@ -286,23 +213,6 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult> {
return null;
}
private void onClearRecentClick(View v){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
prevDisplayItems=new ArrayList<>(displayItems);
refreshing=true;
onDataLoaded(unfilteredResults=Collections.emptyList(), false);
}
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
public void setProgressVisibilityListener(ProgressVisibilityListener progressVisibilityListener){
this.progressVisibilityListener=progressVisibilityListener;
}
@Override
public void onScrollStarted(){
super.onScrollStarted();

View File

@@ -0,0 +1,574 @@
package org.joinmastodon.android.fragments.discover;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.app.Fragment;
import android.graphics.Outline;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.RoundedCorner;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.WindowInsets;
import android.view.animation.AnimationUtils;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.search.GetSearchResults;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.SearchResults;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.model.viewmodel.SearchResultViewModel;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.SearchViewHelper;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.CustomTransitionsFragment;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class SearchQueryFragment extends MastodonRecyclerFragment<SearchResultViewModel> implements CustomTransitionsFragment, OnBackPressedListener{
private static final Pattern HASHTAG_REGEX=Pattern.compile("^(\\w*[a-zA-Z·]\\w*)$", Pattern.CASE_INSENSITIVE);
private static final Pattern USERNAME_REGEX=Pattern.compile("^@?([a-z0-9_-]+)(@[^\\s]+)?$", Pattern.CASE_INSENSITIVE);
private MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
private HideableSingleViewRecyclerAdapter recentsHeader;
private ListItem<Void> openUrlItem, goToHashtagItem, goToAccountItem, goToStatusSearchItem, goToAccountSearchItem;
private ArrayList<ListItem<Void>> topOptions=new ArrayList<>();
private GenericListItemsAdapter<Void> topOptionsAdapter;
private String accountID;
private SearchViewHelper searchViewHelper;
private String currentQuery;
private LayerDrawable navigationIcon;
private Drawable searchIcon, backIcon;
public SearchQueryFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
setRefreshEnabled(false);
setEmptyText("");
openUrlItem=new ListItem<>(R.string.search_open_url, 0, R.drawable.ic_fluent_link_24_regular, this::onOpenURLClick);
goToHashtagItem=new ListItem<>("", null, R.drawable.ic_fluent_number_symbol_24_regular, this::onGoToHashtagClick);
goToAccountItem=new ListItem<>("", null, R.drawable.ic_fluent_person_24_regular, this::onGoToAccountClick);
goToStatusSearchItem=new ListItem<>("", null, R.drawable.ic_fluent_search_24_regular, this::onGoToStatusSearchClick);
goToAccountSearchItem=new ListItem<>("", null, R.drawable.ic_fluent_people_24_regular, this::onGoToAccountSearchClick);
currentQuery=getArguments().getString("query");
dataLoaded();
doLoadData(0, 0);
}
@Override
protected void doLoadData(int offset, int count){
if(isInRecentMode()){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().getRecentSearches(results->{
if(getActivity()==null)
return;
onDataLoaded(results.stream().map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, true);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
}).collect(Collectors.toList()), false);
recentsHeader.setVisible(!data.isEmpty());
});
}else{
currentRequest=new GetSearchResults(currentQuery, null, false)
.limit(2)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(SearchResults result){
onDataLoaded(Stream.of(result.hashtags.stream().map(SearchResult::new), result.accounts.stream().map(SearchResult::new))
.flatMap(Function.identity())
.map(sr->{
SearchResultViewModel vm=new SearchResultViewModel(sr, accountID, false);
if(sr.type==SearchResult.Type.HASHTAG){
vm.hashtagItem.onClick=()->openHashtag(sr);
}
return vm;
})
.collect(Collectors.toList()), false);
recentsHeader.setVisible(false);
}
})
.exec(accountID);
}
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View header=getActivity().getLayoutInflater().inflate(R.layout.display_item_section_header, list, false);
TextView title=header.findViewById(R.id.title);
Button action=header.findViewById(R.id.action_btn);
title.setText(R.string.recent_searches);
action.setText(R.string.clear_all);
action.setOnClickListener(v->onClearRecentClick());
recentsHeader=new HideableSingleViewRecyclerAdapter(header);
mergeAdapter.addAdapter(recentsHeader);
mergeAdapter.addAdapter(topOptionsAdapter=new GenericListItemsAdapter<>(topOptions));
mergeAdapter.addAdapter(new SearchResultsAdapter());
return mergeAdapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
searchViewHelper=new SearchViewHelper(getActivity(), getToolbarContext(), getString(R.string.search_mastodon));
searchViewHelper.setListeners(this::onQueryChanged, this::onQueryChangedNoDebounce);
searchViewHelper.addDivider(contentView);
searchViewHelper.setEnterCallback(this::onSearchViewEnter);
navigationIcon=new LayerDrawable(new Drawable[]{
searchIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_fluent_search_24_regular, getToolbarContext().getTheme()).mutate(),
backIcon=getToolbarContext().getResources().getDrawable(R.drawable.ic_fluent_arrow_left_24_regular, getToolbarContext().getTheme()).mutate()
}){
@Override
public Drawable mutate(){
return this;
}
};
super.onViewCreated(view, savedInstanceState);
int color=UiUtils.alphaBlendThemeColors(getActivity(), R.attr.colorM3Surface, R.attr.colorM3Primary, 0.11f);
view.setBackgroundColor(color);
setStatusBarColor(color);
setNavigationBarColor(color);
if(currentQuery!=null){
searchViewHelper.setQuery(currentQuery);
searchIcon.setAlpha(0);
}
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->!isInRecentMode() && vh.getAbsoluteAdapterPosition()==topOptions.size()-1));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
((ViewGroup.MarginLayoutParams)getToolbar().getLayoutParams()).topMargin=V.dp(8);
searchViewHelper.install(getToolbar());
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
private void onQueryChanged(String q){
currentQuery=q;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
}
refreshing=true;
doLoadData(0, 0);
}
private void onQueryChangedNoDebounce(String q){
updateTopOptions(q);
if(!TextUtils.isEmpty(q)){
recentsHeader.setVisible(false);
}
data.clear();
mergeAdapter.notifyDataSetChanged();
}
private void updateTopOptions(String q){
topOptions.clear();
// https://github.com/mastodon/mastodon/blob/a985d587e13494b78ef2879e4d97f78a2df693db/app/javascript/mastodon/features/compose/components/search.jsx#L233
String trimmedValue=q.trim();
if(trimmedValue.length()>0){
boolean couldBeURL=trimmedValue.startsWith("https://") && !trimmedValue.contains(" ");
if(couldBeURL){
topOptions.add(openUrlItem);
}
boolean couldBeHashtag=(trimmedValue.startsWith("#") && trimmedValue.length()>1 && !trimmedValue.contains(" ")) || HASHTAG_REGEX.matcher(trimmedValue).find();
if(couldBeHashtag){
String tag=trimmedValue.startsWith("#") ? trimmedValue.substring(1) : trimmedValue;
goToHashtagItem.title=getString(R.string.posts_matching_hashtag, "#"+tag);
topOptions.add(goToHashtagItem);
}
Matcher usernameMatcher=USERNAME_REGEX.matcher(trimmedValue);
if(usernameMatcher.find()){
String username="@"+usernameMatcher.group(1);
String atDomain=usernameMatcher.group(2);
if(atDomain==null){
username+="@"+AccountSessionManager.get(accountID).domain;
}
goToAccountItem.title=getString(R.string.search_go_to_account, username);
topOptions.add(goToAccountItem);
}
goToStatusSearchItem.title=getString(R.string.posts_matching_string, trimmedValue);
topOptions.add(goToStatusSearchItem);
goToAccountSearchItem.title=getString(R.string.accounts_matching_string, trimmedValue);
topOptions.add(goToAccountSearchItem);
}
topOptionsAdapter.notifyDataSetChanged();
}
@Override
public Animator onCreateEnterTransition(View prev, View container){
return createTransition(prev, container, true);
}
@Override
public Animator onCreateExitTransition(View prev, View container){
return createTransition(prev, container, false);
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
@Override
protected Drawable getNavigationIconDrawable(){
return navigationIcon;
}
@Override
protected void onShown(){
super.onShown();
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchViewHelper.getSearchEdit(), 0);
}
@Override
protected void onHidden(){
super.onHidden();
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(getActivity().getWindow().getDecorView().getWindowToken(), 0);
}
@RequiresApi(api = Build.VERSION_CODES.S)
private float getScreenCornerRadius(WindowInsets insets, int pos){
RoundedCorner corner=insets.getRoundedCorner(pos);
if(corner==null)
return 0;
return corner.getRadius();
}
private Animator createTransition(View prev, View container, boolean enter){
int[] loc={0, 0};
View searchBtn=prev.findViewById(R.id.search_wrap);
searchBtn.getLocationInWindow(loc);
int btnLeft=loc[0], btnTop=loc[1];
container.getLocationInWindow(loc);
int offX=btnLeft-loc[0], offY=btnTop-loc[1];
float screenRadius;
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S){
WindowInsets insets=container.getRootWindowInsets();
screenRadius=Math.min(
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_TOP_RIGHT)),
Math.min(getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_LEFT), getScreenCornerRadius(insets, RoundedCorner.POSITION_BOTTOM_RIGHT))
);
}else{
screenRadius=0;
}
float buttonRadius=V.dp(26);
Rect buttonBounds=new Rect(offX, offY, offX+searchBtn.getWidth(), offY+searchBtn.getHeight());
Rect containerBounds=new Rect(0, 0, container.getWidth(), container.getHeight());
AnimatableOutlineProvider outlineProvider=new AnimatableOutlineProvider(enter ? buttonBounds : containerBounds, enter ? containerBounds : buttonBounds, enter ? buttonRadius : screenRadius);
container.setOutlineProvider(outlineProvider);
container.setClipToOutline(true);
AnimatorSet set=new AnimatorSet();
ObjectAnimator boundsAnim;
Toolbar toolbar=getToolbar();
float toolbarTX=offX-toolbar.getX();
float toolbarTY=offY-toolbar.getY()+(searchBtn.getHeight()-toolbar.getHeight())/2f;
ArrayList<Animator> anims=new ArrayList<>();
anims.add(boundsAnim=ObjectAnimator.ofFloat(outlineProvider, "boundsFraction", 0f, 1f));
anims.add(ObjectAnimator.ofFloat(outlineProvider, "radius", enter ? buttonRadius : screenRadius, enter ? screenRadius : buttonRadius));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_X, enter ? toolbarTX : 0, enter ? 0 : toolbarTX));
anims.add(ObjectAnimator.ofFloat(toolbar, View.TRANSLATION_Y, enter ? toolbarTY : 0, enter ? 0 : toolbarTY));
anims.add(ObjectAnimator.ofFloat(searchViewHelper.getDivider(), View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
View parentContent=prev.findViewById(R.id.discover_content);
View parentContentParent=(View) parentContent.getParent();
parentContentParent.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
if(enter){
anims.add(ObjectAnimator.ofFloat(contentWrap, View.TRANSLATION_Y, V.dp(-16), 0));
}else{
}
anims.add(ObjectAnimator.ofFloat(contentWrap, View.ALPHA, enter ? 0 : 1, enter ? 1 : 0));
for(Animator anim:anims){
anim.setDuration(enter ? 700 : 300);
}
if(TextUtils.isEmpty(currentQuery)){
anims.add(ObjectAnimator.ofInt(searchIcon, "alpha", enter ? 255 : 0, enter ? 0 : 255).setDuration(200));
anims.add(ObjectAnimator.ofInt(backIcon, "alpha", enter ? 0 : 255, enter ? 255 : 0).setDuration(200));
}
ObjectAnimator parentContentFade;
anims.add(parentContentFade=ObjectAnimator.ofFloat(parentContent, View.ALPHA, enter ? 1 : 0, enter ? 0 : 1).setDuration(enter ? 350 : 250));
if(!enter){
parentContentFade.setStartDelay(50);
ObjectAnimator parentContentTY;
anims.add(parentContentTY=ObjectAnimator.ofFloat(parentContent, View.TRANSLATION_Y, V.dp(16), 0).setDuration(250));
parentContentTY.setStartDelay(50);
}
set.playTogether(anims);
set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_emphasized_decelerate));
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
container.setOutlineProvider(null);
container.setClipToOutline(false);
parentContentParent.setBackground(null);
}
});
boundsAnim.addUpdateListener(animation->{
container.invalidateOutline();
navigationIcon.invalidateSelf();
});
return set;
}
private void openHashtag(SearchResult res){
UiUtils.openHashtagTimeline(getActivity(), accountID, res.hashtag.name, res.hashtag.following);
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(res);
}
private boolean isInRecentMode(){
return TextUtils.isEmpty(currentQuery);
}
private void onSearchViewEnter(){
deliverResult(currentQuery, null);
}
private void onOpenURLClick(){
UiUtils.openURL(getContext(), accountID, searchViewHelper.getQuery(), false);
}
private void onGoToHashtagClick(){
String q=searchViewHelper.getQuery();
if(q.startsWith("#"))
q=q.substring(1);
UiUtils.openHashtagTimeline(getActivity(), accountID, q, null);
}
private void onGoToAccountClick(){
String q=searchViewHelper.getQuery();
if(!q.startsWith("@")){
q="@"+q;
}
if(q.lastIndexOf('@')==0){
q+="@"+AccountSessionManager.get(accountID).domain;
}
UiUtils.lookupAccountHandle(getContext(), accountID, q, (clazz, args) -> {
if (!args.containsKey("profileAccount")) {
Toast.makeText(getContext(), R.string.no_search_results, Toast.LENGTH_SHORT).show();
return;
}
Nav.go((Activity) getContext(), ProfileFragment.class, args);
}).ifPresent(progress -> progress.wrapProgress((Activity) getContext(), R.string.loading, true));
}
private void onGoToStatusSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.STATUS);
}
private void onGoToAccountSearchClick(){
deliverResult(searchViewHelper.getQuery(), SearchResult.Type.ACCOUNT);
}
private void onClearRecentClick(){
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().clearRecentSearches();
if(isInRecentMode()){
data.clear();
recentsHeader.setVisible(false);
mergeAdapter.notifyDataSetChanged();
}
}
private void deliverResult(String query, SearchResult.Type typeFilter){
Bundle res=new Bundle();
res.putString("query", query);
if(typeFilter!=null)
res.putInt("filter", typeFilter.ordinal());
setResult(true, res);
Nav.finish(this);
}
@Override
public boolean onBackPressed(){
String initialQuery=getArguments().getString("query");
searchViewHelper.setQuery(TextUtils.isEmpty(initialQuery) ? "" : initialQuery);
currentQuery=initialQuery;
return false;
}
private static class AnimatableOutlineProvider extends ViewOutlineProvider{
private float boundsFraction, radius;
private final Rect boundsFrom, boundsTo;
private AnimatableOutlineProvider(Rect boundsFrom, Rect boundsTo, float radius){
this.boundsFrom=boundsFrom;
this.boundsTo=boundsTo;
this.radius=radius;
}
@Override
public void getOutline(View view, Outline outline){
outline.setRoundRect(
UiUtils.lerp(boundsFrom.left, boundsTo.left, boundsFraction),
UiUtils.lerp(boundsFrom.top, boundsTo.top, boundsFraction),
UiUtils.lerp(boundsFrom.right, boundsTo.right, boundsFraction),
UiUtils.lerp(boundsFrom.bottom, boundsTo.bottom, boundsFraction),
radius
);
}
@Keep
public float getBoundsFraction(){
return boundsFraction;
}
@Keep
public void setBoundsFraction(float boundsFraction){
this.boundsFraction=boundsFraction;
}
@Keep
public float getRadius(){
return radius;
}
@Keep
public void setRadius(float radius){
this.radius=radius;
}
}
private class SearchResultsAdapter extends UsableRecyclerView.Adapter<RecyclerView.ViewHolder> implements ImageLoaderRecyclerAdapter{
public SearchResultsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
if(viewType==R.id.list_item_account){
return new CustomAccountViewHolder(SearchQueryFragment.this, parent, null);
}else if(viewType==R.id.list_item_simple){
return new SimpleListItemViewHolder(parent.getContext(), parent);
}
throw new IllegalArgumentException();
}
@Override
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position){
if(holder instanceof CustomAccountViewHolder avh){
avh.bind(data.get(position).account);
avh.searchResult=data.get(position).result;
}else if(holder instanceof SimpleListItemViewHolder ivh){
ivh.bind(data.get(position).hashtagItem);
}
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public int getItemViewType(int position){
return switch(data.get(position).result.type){
case ACCOUNT -> R.id.list_item_account;
case HASHTAG -> R.id.list_item_simple;
default -> throw new IllegalStateException("Unexpected value: "+data.get(position).result.type);
};
}
@Override
public int getImageCountForItem(int position){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null)
return vm.account.emojiHelper.getImageCount()+1;
return 0;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
SearchResultViewModel vm=data.get(position);
if(vm.account!=null){
if(image==0)
return vm.account.avaRequest;
return vm.account.emojiHelper.getImageRequest(image-1);
}
return null;
}
}
private class CustomAccountViewHolder extends AccountViewHolder{
public SearchResult searchResult;
public CustomAccountViewHolder(Fragment fragment, ViewGroup list, HashMap<String, Relationship> relationships){
super(fragment, list, relationships);
setStyle(AccessoryType.NONE, false);
}
@Override
public void onClick(){
super.onClick();
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putRecentSearch(searchResult);
}
}
}

View File

@@ -1,9 +1,5 @@
package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@@ -11,29 +7,26 @@ import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.trends.GetTrendingHashtags;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.HashtagChartView;
import org.joinmastodon.android.utils.ProvidesAssistContent;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.BaseRecyclerFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.views.UsableRecyclerView;
public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
public class TrendingHashtagsFragment extends BaseRecyclerFragment<Hashtag> implements ScrollableToTop{
private String accountID;
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_HASHTAGS);
public DiscoverHashtagsFragment(){
public TrendingHashtagsFragment(){
super(10);
}
@@ -49,7 +42,6 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Hashtag> result){
if (getActivity() == null) return;
onDataLoaded(result, false);
}
})
@@ -61,33 +53,11 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
return new HashtagsAdapter();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, .5f, 16, 16));
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
}
@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}
@Override
public String getAccountID() {
return accountID;
}
@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/tags").build();
}
private class HashtagsAdapter extends RecyclerView.Adapter<HashtagViewHolder>{
@NonNull
@Override
@@ -123,11 +93,9 @@ public class DiscoverHashtagsFragment extends RecyclerFragment<Hashtag> implemen
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
import android.annotation.SuppressLint;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -23,8 +22,7 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.SettingsFragment;
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.ui.AccountSwitcherSheet;
import org.joinmastodon.android.ui.utils.UiUtils;
@@ -38,7 +36,6 @@ import me.grishka.appkit.api.APIRequest;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.V;
public class AccountActivationFragment extends ToolbarFragment{
private String accountID;
@@ -50,6 +47,7 @@ public class AccountActivationFragment extends ToolbarFragment{
private APIRequest currentRequest;
private Runnable resendTimer=this::updateResendTimer;
private long lastResendTime;
private boolean visible;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -70,7 +68,7 @@ public class AccountActivationFragment extends ToolbarFragment{
openEmailBtn.setOnLongClickListener(v->{
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsFragment.class, args);
Nav.go(getActivity(), SettingsMainFragment.class, args);
return true;
});
resendBtn=view.findViewById(R.id.btn_resend);
@@ -108,24 +106,20 @@ public class AccountActivationFragment extends ToolbarFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
contentView.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(contentView, insets));
}
@Override
protected void onShown(){
super.onShown();
visible=true;
tryGetAccount();
}
@Override
protected void onHidden(){
super.onHidden();
visible=false;
if(currentRequest!=null){
currentRequest.cancel();
currentRequest=null;
@@ -238,6 +232,8 @@ public class AccountActivationFragment extends ToolbarFragment{
}
private void proceed(){
if(!visible)
return;
Bundle args=new Bundle();
args.putString("account", accountID);
// Nav.goClearingStack(getActivity(), HomeFragment.class, args);

View File

@@ -122,7 +122,7 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorWindowBackground));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Surface));
list.setItemAnimator(new BetterItemAnimator());
((UsableRecyclerView) list).setSelector(null);
}
@@ -131,17 +131,15 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
protected void doLoadData(int offset, int count) {}
@Override
protected RecyclerView.Adapter getAdapter(){
protected RecyclerView.Adapter<?> getAdapter(){
headerView=getActivity().getLayoutInflater().inflate(R.layout.header_welcome_custom, list, false);
searchEdit=headerView.findViewById(R.id.search_edit);
searchEdit.setOnEditorActionListener(this::onSearchEnterPressed);
headerView.findViewById(R.id.more).setVisibility(View.GONE);
headerView.findViewById(R.id.visibility).setVisibility(View.GONE);
headerView.findViewById(R.id.separator).setVisibility(View.GONE);
headerView.findViewById(R.id.timestamp).setVisibility(View.GONE);
headerView.findViewById(R.id.unread_indicator).setVisibility(View.GONE);
((TextView) headerView.findViewById(R.id.username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.time_and_username)).setText(R.string.sk_app_username);
((TextView) headerView.findViewById(R.id.name)).setText(R.string.sk_app_name);
((ImageView) headerView.findViewById(R.id.avatar)).setImageDrawable(getActivity().getDrawable(R.mipmap.ic_launcher));
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(this);
@@ -203,7 +201,6 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
private class InstanceViewHolder extends BindableViewHolder<CatalogInstance> implements UsableRecyclerView.Clickable{
private final TextView title, description, userCount, lang;
private final RadioButton radioButton;
public InstanceViewHolder(){
super(getActivity(), R.layout.item_instance_custom, list);
@@ -211,7 +208,6 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
description=findViewById(R.id.description);
userCount=findViewById(R.id.user_count);
lang=findViewById(R.id.lang);
radioButton=findViewById(R.id.radiobtn);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(userCount);
UiUtils.fixCompoundDrawableTintOnAndroid6(lang);
@@ -231,22 +227,10 @@ public class CustomWelcomeFragment extends InstanceCatalogFragment {
userCount.setText(UiUtils.abbreviateNumber(item.totalUsers));
lang.setText(item.language.toUpperCase());
}
radioButton.setChecked(chosenInstance==item);
radioButton.setVisibility(View.GONE);
}
@Override
public void onClick(){
if(chosenInstance!=null){
int idx=filteredData.indexOf(chosenInstance);
if(idx!=-1){
RecyclerView.ViewHolder holder=list.findViewHolderForAdapterPosition(mergeAdapter.getPositionForAdapter(adapter)+idx);
if(holder instanceof InstanceViewHolder ivh){
ivh.radioButton.setChecked(false);
}
}
}
radioButton.setChecked(true);
if(chosenInstance==null)
nextButton.setEnabled(true);
chosenInstance=item;

View File

@@ -75,7 +75,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
items.add(new Item("Mastodon for Android Privacy Policy", getString(R.string.privacy_policy_explanation), "joinmastodon.org", "https://joinmastodon.org/android/privacy", "https://joinmastodon.org/favicon-32x32.png"));
@@ -123,16 +123,12 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
@@ -155,13 +151,7 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void loadServerPrivacyPolicy(){

View File

@@ -17,10 +17,11 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.catalog.CatalogInstance;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
@@ -52,7 +53,7 @@ import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
abstract class InstanceCatalogFragment extends RecyclerFragment<CatalogInstance> {
abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogInstance> {
protected RecyclerView.Adapter adapter;
protected MergeRecyclerAdapter mergeAdapter;
protected CatalogInstance chosenInstance;
@@ -227,7 +228,7 @@ abstract class InstanceCatalogFragment extends RecyclerFragment<CatalogInstance>
}
loadingInstanceDomain=null;
showInstanceInfoLoadError(domain, error);
if(fakeInstance!=null){
if(fakeInstance!=null && getActivity()!=null){
fakeInstance.description=getString(R.string.error);
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
if(list.findViewHolderForAdapterPosition(1) instanceof BindableViewHolder<?> ivh){
@@ -330,13 +331,7 @@ abstract class InstanceCatalogFragment extends RecyclerFragment<CatalogInstance>
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override

View File

@@ -164,7 +164,6 @@ public class InstanceChooserLoginFragment extends InstanceCatalogFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override

View File

@@ -60,7 +60,7 @@ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAs
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
setTitle(R.string.instance_rules_title);
}
@@ -79,7 +79,7 @@ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAs
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 56, 0, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(v->onButtonClick());

View File

@@ -1,61 +1,34 @@
package org.joinmastodon.android.fragments.onboarding;
import android.app.ProgressDialog;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.RecyclerFragment;
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
import org.joinmastodon.android.model.FollowSuggestion;
import org.joinmastodon.android.model.ParsedAccount;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.ProgressBarButton;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.utils.ElevationOnScrollListener;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.FragmentRootLinearLayout;
import me.grishka.appkit.views.UsableRecyclerView;
public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<ParsedAccount> {
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;
private View buttonBar;
private ElevationOnScrollListener onScrollListener;
private int numRunningFollowRequests=0;
@@ -77,8 +50,6 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
buttonBar=view.findViewById(R.id.button_bar);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
@@ -88,8 +59,6 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
@@ -101,51 +70,15 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
onDataLoaded(result.stream().map(fs->new ParsedAccount(fs.account, accountID)).collect(Collectors.toList()), false);
loadRelationships();
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
}
})
.exec(accountID);
}
private void loadRelationships(){
relationships=Collections.emptyMap();
relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList()));
relationshipsRequest.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof SuggestionViewHolder svh)
svh.rebind();
}
}
@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
@Override
protected RecyclerView.Adapter getAdapter(){
return new SuggestionsAdapter();
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onFollowAllClick(View v){
@@ -156,7 +89,7 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
return;
}
ArrayList<String> accountIdsToFollow=new ArrayList<>();
for(ParsedAccount acc:data){
for(AccountViewModel acc:data){
Relationship rel=relationships.get(acc.account.id);
if(rel==null)
continue;
@@ -193,7 +126,7 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
public void onSuccess(Relationship result){
relationships.put(id, result);
for(int i=0;i<list.getChildCount();i++){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof SuggestionViewHolder svh && svh.getItem().account.id.equals(id)){
if(list.getChildViewHolder(list.getChildAt(i)) instanceof AccountViewHolder svh && svh.getItem().account.id.equals(id)){
svh.rebind();
break;
}
@@ -219,126 +152,14 @@ public class OnboardingFollowSuggestionsFragment extends RecyclerFragment<Parsed
Nav.go(getActivity(), OnboardingProfileSetupFragment.class, args);
}
private class SuggestionsAdapter extends UsableRecyclerView.Adapter<SuggestionViewHolder> implements ImageLoaderRecyclerAdapter{
public SuggestionsAdapter(){
super(imgLoader);
}
@NonNull
@Override
public SuggestionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new SuggestionViewHolder();
}
@Override
public int getItemCount(){
return data.size();
}
@Override
public void onBindViewHolder(SuggestionViewHolder holder, int position){
holder.bind(data.get(position));
super.onBindViewHolder(holder, position);
}
@Override
public int getImageCountForItem(int position){
return data.get(position).emojiHelper.getImageCount()+1;
}
@Override
public ImageLoaderRequest getImageRequest(int position, int image){
ParsedAccount account=data.get(position);
if(image==0)
return account.avatarRequest;
return account.emojiHelper.getImageRequest(image-1);
}
@Override
protected void onConfigureViewHolder(AccountViewHolder holder){
super.onConfigureViewHolder(holder);
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
}
private class SuggestionViewHolder extends BindableViewHolder<ParsedAccount> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
private final TextView name, username, bio;
private final ImageView avatar;
private final ProgressBarButton actionButton;
private final ProgressBar actionProgress;
private final View actionWrap;
private Relationship relationship;
public SuggestionViewHolder(){
super(getActivity(), R.layout.item_user_row_m3, list);
name=findViewById(R.id.name);
username=findViewById(R.id.username);
bio=findViewById(R.id.bio);
avatar=findViewById(R.id.avatar);
actionButton=findViewById(R.id.action_btn);
actionProgress=findViewById(R.id.action_progress);
actionWrap=findViewById(R.id.action_btn_wrap);
avatar.setOutlineProvider(OutlineProviders.roundedRect(10));
avatar.setClipToOutline(true);
actionButton.setOnClickListener(UiUtils.rateLimitedClickListener(this::onActionButtonClick));
}
@Override
public void onBind(ParsedAccount item){
name.setText(item.parsedName);
username.setText(item.account.getDisplayUsername());
if(TextUtils.isEmpty(item.parsedBio)){
bio.setVisibility(View.GONE);
}else{
bio.setVisibility(View.VISIBLE);
bio.setText(item.parsedBio);
}
relationship=relationships.get(item.account.id);
if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
}
}
@Override
public void setImage(int index, Drawable image){
if(index==0){
avatar.setImageDrawable(image);
}else{
item.emojiHelper.setImageDrawable(index-1, image);
name.invalidate();
bio.invalidate();
}
if(image instanceof Animatable a && !a.isRunning())
a.start();
}
@Override
public void clearImage(int index){
setImage(index, null);
}
@Override
public void onClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(item.account));
Nav.go(getActivity(), ProfileFragment.class, args);
}
private void onActionButtonClick(View v){
itemView.setHasTransientState(true);
UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{
itemView.setHasTransientState(false);
relationships.put(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 Uri getWebUri(Uri.Builder base){
return null;
}
}

View File

@@ -60,7 +60,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
accountID=getArguments().getString("account");
setTitle(R.string.profile_setup);
}
@@ -118,16 +118,12 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
scroller.setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
@@ -165,13 +161,7 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private View makeFieldsRow(){

View File

@@ -45,6 +45,7 @@ import org.jsoup.nodes.TextNode;
import org.jsoup.select.NodeVisitor;
import org.parceler.Parcels;
import java.time.ZoneId;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -148,16 +149,12 @@ public class SignupFragment extends ToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
setStatusBarColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Background));
view.findViewById(R.id.scroller).setOnScrollChangeListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setBackgroundResource(R.drawable.bg_onboarding_panel);
getToolbar().setElevation(0);
if(onScrollListener!=null){
onScrollListener.setViews(buttonBar, getToolbar());
}
@@ -193,7 +190,7 @@ public class SignupFragment extends ToolbarFragment{
edit.setError(null);
}
errorFields.clear();
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString())
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Token result){
@@ -370,13 +367,7 @@ public class SignupFragment extends ToolbarFragment{
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onGoBackLinkClick(LinkSpan span){

View File

@@ -1,15 +1,13 @@
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import org.joinmastodon.android.E;
@@ -23,14 +21,9 @@ import org.parceler.Parcels;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
@@ -38,12 +31,13 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
private MergeRecyclerAdapter adapter;
private Button btn;
private View buttonBar;
protected ArrayList<Item> items=new ArrayList<>();
protected ArrayList<ChoiceItem> items=new ArrayList<>();
protected boolean isMultipleChoice;
protected ArrayList<String> selectedIDs=new ArrayList<>();
protected String accountID;
protected Account reportAccount;
protected Status reportStatus;
protected ProgressBar progressBar;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -61,7 +55,7 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
@@ -75,122 +69,33 @@ public abstract class BaseReportChoiceFragment extends MastodonToolbarFragment{
list=view.findViewById(R.id.list);
list.setLayoutManager(new LinearLayoutManager(getActivity()));
populateItems();
Item header=getHeaderItem();
ChoiceItem header=getHeaderItem();
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
TextView stepCounter=headerView.findViewById(R.id.step_counter);
title.setText(header.title);
subtitle.setText(header.subtitle);
stepCounter.setText(getString(R.string.step_x_of_n, getStepNumber(), 3));
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ItemsAdapter());
list.setAdapter(adapter);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorPollVoted, 1, 16, 16, DividerItemDecoration.NOT_FIRST));
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
progressBar=view.findViewById(R.id.top_progress);
adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
list.setAdapter(adapter);
return view;
}
protected abstract Item getHeaderItem();
protected abstract ChoiceItem getHeaderItem();
protected abstract void populateItems();
protected abstract void onButtonClick();
protected abstract int getStepNumber();
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
}
protected static class Item{
public String title, subtitle, id;
public Item(String title, String subtitle, String id){
this.title=title;
this.subtitle=subtitle;
this.id=id;
}
}
private class ItemsAdapter extends RecyclerView.Adapter<ItemViewHolder>{
@NonNull
@Override
public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ItemViewHolder();
}
@Override
public void onBindViewHolder(@NonNull ItemViewHolder holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
}
private class ItemViewHolder extends BindableViewHolder<Item> implements UsableRecyclerView.Clickable{
private final TextView title, subtitle;
private final ImageView checkbox;
public ItemViewHolder(){
super(getActivity(), R.layout.item_report_choice, list);
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
}
@Override
public void onBind(Item item){
title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
}
checkbox.setSelected(selectedIDs.contains(item.id));
}
@Override
public void onClick(){
if(isMultipleChoice){
if(selectedIDs.contains(item.id))
selectedIDs.remove(item.id);
else
selectedIDs.add(item.id);
rebind();
}else{
if(!selectedIDs.contains(item.id)){
if(!selectedIDs.isEmpty()){
String prev=selectedIDs.remove(0);
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof ItemViewHolder ivh && ivh.getItem().id.equals(prev)){
ivh.rebind();
break;
}
}
}
selectedIDs.add(item.id);
rebind();
}
}
btn.setEnabled(!selectedIDs.isEmpty());
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.fragments.report;
class ChoiceItem{
public String title, subtitle, id;
public ChoiceItem(String title, String subtitle, String id){
this.title=title;
this.subtitle=subtitle;
this.id=id;
}
}

View File

@@ -0,0 +1,85 @@
package org.joinmastodon.android.fragments.report;
import android.content.Context;
import android.text.TextUtils;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.RadioButton;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.ui.views.CheckableLinearLayout;
import java.util.ArrayList;
import java.util.function.Consumer;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
class ChoiceItemViewHolder extends BindableViewHolder<ChoiceItem> implements UsableRecyclerView.Clickable{
private final TextView title, subtitle;
private final View checkbox;
private final CheckableLinearLayout view;
private final boolean isMultipleChoice;
private final RecyclerView list;
private final ArrayList<String> selectedIDs;
private final Consumer<Boolean> buttonEnabledSetter;
public ChoiceItemViewHolder(Context context, boolean isMultipleChoice, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
super(context, R.layout.item_report_choice, list);
this.buttonEnabledSetter=buttonEnabledSetter;
this.isMultipleChoice=isMultipleChoice;
this.list=list;
this.selectedIDs=selectedIDs;
title=findViewById(R.id.title);
subtitle=findViewById(R.id.subtitle);
checkbox=findViewById(R.id.checkbox);
CompoundButton cb=isMultipleChoice ? new CheckBox(context) : new RadioButton(context);
checkbox.setBackground(cb.getButtonDrawable());
view=(CheckableLinearLayout) itemView;
}
@Override
public void onBind(ChoiceItem item){
title.setText(item.title);
if(TextUtils.isEmpty(item.subtitle)){
subtitle.setVisibility(View.GONE);
view.setMinimumHeight(V.dp(56));
}else{
subtitle.setVisibility(View.VISIBLE);
subtitle.setText(item.subtitle);
view.setMinimumHeight(V.dp(72));
}
view.setChecked(selectedIDs.contains(item.id));
}
@Override
public void onClick(){
if(isMultipleChoice){
if(selectedIDs.contains(item.id))
selectedIDs.remove(item.id);
else
selectedIDs.add(item.id);
rebind();
}else{
if(!selectedIDs.contains(item.id)){
if(!selectedIDs.isEmpty()){
String prev=selectedIDs.remove(0);
for(int i=0; i<list.getChildCount(); i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof ChoiceItemViewHolder ivh && ivh.getItem().id.equals(prev)){
ivh.rebind();
break;
}
}
}
selectedIDs.add(item.id);
rebind();
}
}
buttonEnabledSetter.accept(!selectedIDs.isEmpty());
}
}

View File

@@ -0,0 +1,46 @@
package org.joinmastodon.android.fragments.report;
import android.content.Context;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.function.Consumer;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.views.UsableRecyclerView;
class ChoiceItemsAdapter extends RecyclerView.Adapter<ChoiceItemViewHolder>{
private final Context context;
private final boolean isMultipleChoice;
private final ArrayList<ChoiceItem> items;
private final RecyclerView list;
private final ArrayList<String> selectedIDs;
private final Consumer<Boolean> buttonEnabledSetter;
public ChoiceItemsAdapter(Context context, boolean isMultipleChoice, ArrayList<ChoiceItem> items, RecyclerView list, ArrayList<String> selectedIDs, Consumer<Boolean> buttonEnabledSetter){
this.context=context;
this.isMultipleChoice=isMultipleChoice;
this.items=items;
this.list=list;
this.selectedIDs=selectedIDs;
this.buttonEnabledSetter=buttonEnabledSetter;
}
@NonNull
@Override
public ChoiceItemViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
return new ChoiceItemViewHolder(context, isMultipleChoice, list, selectedIDs, buttonEnabledSetter);
}
@Override
public void onBindViewHolder(@NonNull ChoiceItemViewHolder holder, int position){
holder.bind(items.get(position));
}
@Override
public int getItemCount(){
return items.size();
}
}

View File

@@ -4,14 +4,12 @@ import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.SparseIntArray;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
@@ -22,38 +20,36 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
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.HashSet;
import java.util.List;
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.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class ReportAddPostsChoiceFragment extends StatusListFragment{
private Button btn;
private View buttonBar;
private ArrayList<String> selectedIDs=new ArrayList<>();
private String accountID;
private Account reportAccount;
private Status reportStatus;
private SparseIntArray knownDisplayItemHeights=new SparseIntArray();
private HashSet<String> postsWithKnownNonHeaderHeights=new HashSet<>();
@Override
public void onCreate(Bundle savedInstanceState){
@@ -73,13 +69,15 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
if(reportStatus!=null)
if(reportStatus!=null){
selectedIDs.add(reportStatus.id);
setTitle(getString(R.string.report_title, reportAccount.acct));
setTitle(R.string.report_title_post);
}else{
setTitle(getString(R.string.report_title, reportAccount.acct));
}
loadData();
}
@@ -89,7 +87,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if (getActivity() == null) return;
for(Status s:result){
s.sensitive=true;
}
onDataLoaded(result, !result.isEmpty());
}
})
@@ -102,8 +102,10 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
selectedIDs.remove(id);
else
selectedIDs.add(id);
list.invalidate();
btn.setEnabled(!selectedIDs.isEmpty());
CheckableHeaderStatusDisplayItem.Holder holder=findHolderOfType(id, CheckableHeaderStatusDisplayItem.Holder.class);
if(holder!=null)
holder.rebind();
}
@Override
@@ -112,88 +114,27 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(this::onButtonClick);
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Drawable uncheckedIcon=getResources().getDrawable(R.drawable.ic_fluent_radio_button_24_regular, getActivity().getTheme()).mutate();
private Drawable checkedIcon=getResources().getDrawable(R.drawable.ic_fluent_checkmark_circle_24_filled, getActivity().getTheme()).mutate();
{
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorSecondary);
checkedIcon.setTint(color);
uncheckedIcon.setTint(color);
checkedIcon.setBounds(0, 0, checkedIcon.getIntrinsicWidth(), checkedIcon.getIntrinsicHeight());
uncheckedIcon.setBounds(0, 0, uncheckedIcon.getIntrinsicWidth(), uncheckedIcon.getIntrinsicHeight());
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder.getAbsoluteAdapterPosition()==0)
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
return;
outRect.left=V.dp(40);
if(holder instanceof AudioStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
}else if(holder instanceof LinkCardStatusDisplayItem.Holder){
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.bottom=V.dp(16);
outRect.left+=V.dp(16);
outRect.right=V.dp(16);
}
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
// 1st pass: update item heights
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder sdiHolder){
parent.getDecoratedBoundsWithMargins(child, tmpRect);
String id=sdiHolder.getItemID();
int height=tmpRect.height();
if(!(holder instanceof HeaderStatusDisplayItem.Holder) && !(holder instanceof ReblogOrReplyLineStatusDisplayItem.Holder))
postsWithKnownNonHeaderHeights.add(id);
knownDisplayItemHeights.put(holder.getAbsoluteAdapterPosition(), height);
}
}
// 2nd pass: draw checkboxes
String lastPostID=null;
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
if(holder instanceof StatusDisplayItem.Holder<?> sdiHolder){
String postID=sdiHolder.getItemID();
if(!postID.equals(lastPostID)){
lastPostID=postID;
if(!postsWithKnownNonHeaderHeights.contains(postID))
continue; // We don't know full height of this post yet
int postHeight=0;
int heightOffset=0;
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset();j<displayItems.size();j++){
StatusDisplayItem item=displayItems.get(j);
if(!item.parentID.equals(postID))
break;
postHeight+=knownDisplayItemHeights.get(j+getMainAdapterOffset());
}
for(int j=holder.getAbsoluteAdapterPosition()-getMainAdapterOffset()-1;j>=0;j--){
StatusDisplayItem item=displayItems.get(j);
if(!item.parentID.equals(postID))
break;
int itemHeight=knownDisplayItemHeights.get(j+getMainAdapterOffset());
postHeight+=itemHeight;
heightOffset+=itemHeight;
}
int y=Math.round(child.getY())+postHeight/2-heightOffset;
Drawable check=selectedIDs.contains(postID) ? checkedIcon : uncheckedIcon;
c.save();
c.translate(V.dp(16), y-check.getIntrinsicHeight()/2f);
check.draw(c);
c.restore();
}
}
}
}
});
ProgressBar topProgress=view.findViewById(R.id.top_progress);
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 50 : 33);
}
@Override
@@ -206,10 +147,8 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
View headerView=getActivity().getLayoutInflater().inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
TextView stepCounter=headerView.findViewById(R.id.step_counter);
title.setText(R.string.report_choose_posts);
subtitle.setText(R.string.report_choose_posts_subtitle);
stepCounter.setText(getString(R.string.step_x_of_n, 2, 3));
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
@@ -218,17 +157,14 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
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(V.dp(16), y, parent.getWidth()-V.dp(16), y, paint);
}
private void onButtonClick(View v){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
if(reportStatus!=null)
args.putBoolean("fromPost", true);
if(v.getId()==R.id.btn_next){
args.putStringArrayList("statusIDs", selectedIDs);
}else{
@@ -239,18 +175,13 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
args.putStringArrayList("ruleIDs", getArguments().getStringArrayList("ruleIDs"));
args.putString("reason", getArguments().getString("reason"));
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
Nav.go(getActivity(), ReportCommentFragment.class, args);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Subscribe
@@ -265,14 +196,40 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
}
@Override
protected Filter.FilterContext getFilterContext() {
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
}
@Override
protected FilterContext getFilterContext(){
return null;
}
@Override
public Uri getWebUri(Uri.Builder base) {
if (reportStatus != null) return Uri.parse(reportStatus.url);
if (reportAccount != null) return Uri.parse(reportAccount.url);
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
View layout=h.getLayout();
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
layout.setClipToOutline(true);
View overlay=h.getSensitiveOverlay();
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
overlay.setClipToOutline(true);
}else if((Object)holder instanceof CheckableHeaderStatusDisplayItem.Holder h){
h.setIsChecked(this::isChecked);
}
}
private boolean isChecked(CheckableHeaderStatusDisplayItem.Holder holder){
return selectedIDs.contains(holder.getItem().parentID);
}
@Override
public Uri getWebUri(Uri.Builder base){
return null;
}
}

View File

@@ -1,14 +1,15 @@
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.Switch;
import android.widget.TextView;
@@ -18,6 +19,7 @@ import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.reports.SendReport;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.MastodonToolbarFragment;
import org.joinmastodon.android.model.Account;
@@ -30,17 +32,15 @@ import java.util.ArrayList;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.utils.V;
public class ReportCommentFragment extends MastodonToolbarFragment{
private String accountID;
private Account reportAccount;
private Button btn;
private View buttonBar, forwardReportItem;
private TextView forwardReportText;
private Switch forwardReportSwitch;
private View buttonBar;
private EditText commentEdit;
private boolean forwardReport = GlobalUserPreferences.forwardReportDefault;
private Switch forwardSwitch;
private View forwardBtn;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -58,10 +58,12 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
setTitle(getString(R.string.report_title, reportAccount.acct));
if(getArguments().getBoolean("fromPost", false))
setTitle(R.string.report_title_post);
else
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@@ -71,52 +73,46 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
TextView title=view.findViewById(R.id.title);
TextView subtitle=view.findViewById(R.id.subtitle);
TextView stepCounter=view.findViewById(R.id.step_counter);
title.setText(R.string.report_comment_title);
subtitle.setVisibility(View.GONE);
stepCounter.setText(getString(R.string.step_x_of_n, 3, 3));
btn=view.findViewById(R.id.btn_next);
btn.setOnClickListener(this::onButtonClick);
view.findViewById(R.id.btn_back).setOnClickListener(this::onButtonClick);
buttonBar=view.findViewById(R.id.button_bar);
commentEdit=view.findViewById(R.id.text);
forwardReportSwitch = view.findViewById(R.id.forward_report_switch);
forwardReportItem = view.findViewById(R.id.forward_report);
forwardReportText = view.findViewById(R.id.forward_report_text);
String domain = reportAccount.getDomain();
if (domain == null) {
forwardReportItem.setVisibility(View.GONE);
} else {
forwardReportItem.setOnClickListener(this::onForwardReportClick);
forwardReportText.setText(getActivity().getString(R.string.sk_forward_report_to, domain));
forwardReportSwitch.setChecked(forwardReport);
forwardSwitch=view.findViewById(R.id.forward_switch);
forwardBtn=view.findViewById(R.id.forward_report);
forwardBtn.setOnClickListener(v->forwardSwitch.toggle());
String myDomain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(!TextUtils.isEmpty(reportAccount.getDomain()) && !myDomain.equalsIgnoreCase(reportAccount.getDomain())){
TextView forwardTitle=view.findViewById(R.id.forward_title);
forwardTitle.setText(getString(R.string.forward_report_to_server, reportAccount.getDomain()));
}else{
forwardBtn.setVisibility(View.GONE);
}
return view;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
ProgressBar topProgress=view.findViewById(R.id.top_progress);
topProgress.setProgress(getArguments().containsKey("ruleIDs") ? 75 : 66);
forwardSwitch.setChecked(GlobalUserPreferences.forwardReportDefault);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onButtonClick(View v){
ReportReason reason=ReportReason.valueOf(getArguments().getString("reason"));
ArrayList<String> statusIDs=getArguments().getStringArrayList("statusIDs");
ArrayList<String> ruleIDs=getArguments().getStringArrayList("ruleIDs");
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardReport)
new SendReport(reportAccount.id, reason, statusIDs, ruleIDs, v.getId()==R.id.btn_back ? null : commentEdit.getText().toString(), forwardSwitch.isChecked())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Object result){
@@ -124,6 +120,8 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
args.putString("account", accountID);
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
args.putBoolean("fromPost", getArguments().getBoolean("fromPost", false));
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
Nav.go(getActivity(), ReportDoneFragment.class, args);
buttonBar.postDelayed(()->E.post(new FinishReportFragmentsEvent(reportAccount.id)), 500);
}
@@ -137,11 +135,6 @@ public class ReportCommentFragment extends MastodonToolbarFragment{
.exec(accountID);
}
private void onForwardReportClick(View v) {
forwardReport = !forwardReport;
forwardReportSwitch.setChecked(forwardReport);
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))

View File

@@ -1,12 +1,14 @@
package org.joinmastodon.android.fragments.report;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Build;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.view.animation.PathInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
@@ -23,10 +25,13 @@ import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import androidx.annotation.DrawableRes;
import androidx.dynamicanimation.animation.DynamicAnimation;
import androidx.dynamicanimation.animation.SpringAnimation;
import androidx.dynamicanimation.animation.SpringForce;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.ToolbarFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.CubicBezierInterpolator;
@@ -38,6 +43,13 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
private Button btn;
private View buttonBar;
private ReportReason reason;
private TextView unfollowTitle;
private TextView muteTitle;
private TextView blockTitle;
private View unfollowBtn;
private View muteBtn;
private View blockBtn;
private Relationship relationship;
@Override
public void onCreate(Bundle savedInstanceState){
@@ -48,14 +60,17 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground));
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reason=ReportReason.valueOf(getArguments().getString("reason"));
setTitle(getString(R.string.report_title, reportAccount.acct));
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
if(getArguments().getBoolean("fromPost", false))
setTitle(R.string.report_title_post);
else
setTitle(getString(R.string.report_title, reportAccount.acct));
}
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_report_done, container, false);
@@ -64,10 +79,18 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
TextView subtitle=view.findViewById(R.id.subtitle);
if(reason==ReportReason.PERSONAL){
title.setText(R.string.report_personal_title);
subtitle.setText(R.string.report_personal_subtitle);
if(relationship!=null && relationship.blocking){
subtitle.setText(R.string.report_personal_already_blocked);
}else{
subtitle.setText(R.string.report_personal_subtitle);
}
}else{
title.setText(R.string.report_sent_title);
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
if(relationship!=null && relationship.blocking){
subtitle.setText(R.string.report_sent_already_blocked);
}else{
subtitle.setText(getString(R.string.report_sent_subtitle, '@'+reportAccount.acct));
}
}
btn=view.findViewById(R.id.btn_next);
@@ -76,31 +99,65 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
btn.setText(R.string.done);
if(reason!=ReportReason.PERSONAL){
View doneOverlay=view.findViewById(R.id.reported_overlay);
doneOverlay.setOutlineProvider(OutlineProviders.roundedRect(7));
TextView stamp=view.findViewById(R.id.reported_stamp);
ImageView ava=view.findViewById(R.id.avatar);
ava.setOutlineProvider(OutlineProviders.roundedRect(24));
ava.setClipToOutline(true);
ViewImageLoader.load(ava, null, new UrlImageLoaderRequest(reportAccount.avatar));
doneOverlay.setScaleX(1.5f);
doneOverlay.setScaleY(1.5f);
doneOverlay.setAlpha(0f);
doneOverlay.animate().scaleX(1f).scaleY(1f).alpha(1f).rotation(8.79f).setDuration(300).setStartDelay(300).setInterpolator(CubicBezierInterpolator.DEFAULT).start();
stamp.setAlpha(0f);
stamp.setTranslationX(V.dp(148));
stamp.setTranslationY(V.dp(-296));
stamp.setScaleX(3.5f);
stamp.setScaleY(3.5f);
ObjectAnimator alpha=ObjectAnimator.ofFloat(stamp, View.ALPHA, 1f).setDuration(400);
alpha.setInterpolator(new PathInterpolator(0.16f, 1, 0.3f, 1));
alpha.start();
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_X, 0f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.TRANSLATION_Y, 0f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_X, 1f));
setupSpringAnimation(new SpringAnimation(stamp, DynamicAnimation.SCALE_Y, 1f));
}else{
view.findViewById(R.id.ava_reported).setVisibility(View.GONE);
}
TextView unfollowTitle=view.findViewById(R.id.unfollow_title);
TextView muteTitle=view.findViewById(R.id.mute_title);
TextView blockTitle=view.findViewById(R.id.block_title);
unfollowTitle=view.findViewById(R.id.unfollow_title);
muteTitle=view.findViewById(R.id.mute_title);
blockTitle=view.findViewById(R.id.block_title);
unfollowBtn=view.findViewById(R.id.unfollow_btn);
muteBtn=view.findViewById(R.id.mute_btn);
blockBtn=view.findViewById(R.id.block_btn);
unfollowTitle.setText(getString(R.string.unfollow_user, '@'+reportAccount.acct));
muteTitle.setText(getString(R.string.mute_user, '@'+reportAccount.acct));
blockTitle.setText(getString(R.string.block_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_fluent_person_delete_20_filled, unfollowTitle);
setIconToButton(R.drawable.ic_fluent_person_prohibited_20_filled, blockTitle);
setIconToButton(R.drawable.ic_fluent_speaker_0_20_filled, muteTitle);
view.findViewById(R.id.unfollow_btn).setOnClickListener(v->onUnfollowClick());
view.findViewById(R.id.mute_btn).setOnClickListener(v->onMuteClick());
view.findViewById(R.id.block_btn).setOnClickListener(v->onBlockClick());
unfollowBtn.setOnClickListener(v->onUnfollowClick());
muteBtn.setOnClickListener(v->onMuteClick());
blockBtn.setOnClickListener(v->onBlockClick());
if(relationship!=null){
if(relationship.blocking){
muteBtn.setVisibility(View.GONE);
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
unfollowBtn.setVisibility(View.GONE);
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
blockBtn.setVisibility(View.GONE);
view.findViewById(R.id.block_explanation).setVisibility(View.GONE);
}else{
if(relationship.muting){
muteBtn.setVisibility(View.GONE);
view.findViewById(R.id.mute_explanation).setVisibility(View.GONE);
}
if(!relationship.following){
unfollowBtn.setVisibility(View.GONE);
view.findViewById(R.id.unfollow_explanation).setVisibility(View.GONE);
}
}
}
return view;
}
@@ -108,18 +165,11 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
view.setBackgroundColor(UiUtils.getThemeColor(getActivity(), android.R.attr.colorBackground));
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=27){
int inset=insets.getSystemWindowInsetBottom();
buttonBar.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0);
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0));
}else{
super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom()));
}
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
private void onButtonClick(View v){
@@ -127,12 +177,17 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onUnfollowClick(){
new SetAccountFollowed(reportAccount.id, false, false, false)
new SetAccountFollowed(reportAccount.id, false, false)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
Nav.finish(ReportDoneFragment.this);
E.post(new RemoveAccountPostsEvent(accountID, reportAccount.id, true));
unfollowTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
unfollowTitle.setText(getString(R.string.unfollowed_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_fluent_checkmark_24_regular, unfollowTitle);
unfollowBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
unfollowBtn.setClickable(false);
unfollowBtn.setFocusable(false);
}
@Override
@@ -145,10 +200,50 @@ public class ReportDoneFragment extends MastodonToolbarFragment{
}
private void onMuteClick(){
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
UiUtils.confirmToggleMuteUser(getActivity(), accountID, reportAccount, false, rel->{
muteTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
muteTitle.setText(getString(R.string.muted_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_fluent_checkmark_24_regular, muteTitle);
muteBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
muteBtn.setClickable(false);
muteBtn.setFocusable(false);
});
}
private void onBlockClick(){
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->Nav.finish(this));
UiUtils.confirmToggleBlockUser(getActivity(), accountID, reportAccount, false, rel->{
blockTitle.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSecondaryContainer));
blockTitle.setText(getString(R.string.blocked_user, '@'+reportAccount.acct));
setIconToButton(R.drawable.ic_fluent_checkmark_24_regular, blockTitle);
blockBtn.setBackgroundResource(R.drawable.bg_button_m3_tonal);
blockBtn.setClickable(false);
blockBtn.setFocusable(false);
if(unfollowBtn.isClickable())
unfollowBtn.setEnabled(false);
if(muteBtn.isClickable())
muteBtn.setEnabled(false);
});
}
@Override
protected int getNavigationIconDrawableResource(){
return R.drawable.ic_baseline_close_24;
}
@Override
public boolean wantsCustomNavigationIcon(){
return true;
}
private void setIconToButton(@DrawableRes int icon, TextView button){
Drawable d=getResources().getDrawable(icon, getActivity().getTheme()).mutate();
d.setBounds(0, 0, V.dp(18), V.dp(18));
d.setTintList(button.getTextColors());
button.setCompoundDrawablesRelative(d, null, null, null);
}
private void setupSpringAnimation(SpringAnimation anim){
anim.getSpring().setDampingRatio(SpringForce.DAMPING_RATIO_LOW_BOUNCY).setStiffness(500);
anim.start();
}
}

View File

@@ -1,36 +1,108 @@
package org.joinmastodon.android.fragments.report;
import android.app.Activity;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.FinishReportFragmentsEvent;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.model.ReportReason;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.parceler.Parcels;
import me.grishka.appkit.Nav;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.BindableViewHolder;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.UsableRecyclerView;
public class ReportReasonChoiceFragment extends StatusListFragment{
private MergeRecyclerAdapter mergeAdapter;
private Button btn;
private View buttonBar;
protected ArrayList<ChoiceItem> items=new ArrayList<>();
protected boolean isMultipleChoice;
protected ArrayList<String> selectedIDs=new ArrayList<>();
protected Account reportAccount;
protected Status reportStatus;
protected ProgressBar progressBar;
private Relationship relationship;
public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
@Override
protected Item getHeaderItem(){
return new Item(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct), getString(R.string.report_choose_reason_subtitle), null);
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
setListLayoutId(R.layout.fragment_content_report_posts);
setLayout(R.layout.fragment_report_posts);
E.register(this);
}
@Override
protected void populateItems(){
items.add(new Item(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
items.add(new Item(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
public void onDestroy(){
E.unregister(this);
super.onDestroy();
}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
setNavigationBarColor(UiUtils.getThemeColor(activity, R.attr.colorM3Surface));
accountID=getArguments().getString("account");
reportAccount=Parcels.unwrap(getArguments().getParcelable("reportAccount"));
reportStatus=Parcels.unwrap(getArguments().getParcelable("status"));
if(reportStatus!=null){
Status hiddenStatus=reportStatus.clone();
hiddenStatus.spoilerText=getString(R.string.post_hidden);
onDataLoaded(Collections.singletonList(hiddenStatus));
setTitle(R.string.report_title_post);
}else{
onDataLoaded(Collections.emptyList());
setTitle(getString(R.string.report_title, reportAccount.acct));
}
relationship=Parcels.unwrap(getArguments().getParcelable("relationship"));
if(relationship==null && reportStatus==null)
loadRelationships(Collections.singleton(reportAccount.id));
items.add(new ChoiceItem(getString(R.string.report_reason_personal), getString(R.string.report_reason_personal_subtitle), ReportReason.PERSONAL.name()));
items.add(new ChoiceItem(getString(R.string.report_reason_spam), getString(R.string.report_reason_spam_subtitle), ReportReason.SPAM.name()));
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
if(inst!=null && inst.rules!=null && !inst.rules.isEmpty()){
items.add(new Item(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
items.add(new ChoiceItem(getString(R.string.report_reason_violation), getString(R.string.report_reason_violation_subtitle), ReportReason.VIOLATION.name()));
}
items.add(new Item(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
items.add(new ChoiceItem(getString(R.string.report_reason_other), getString(R.string.report_reason_other_subtitle), ReportReason.OTHER.name()));
}
@Override
protected void onButtonClick(){
ReportReason reason=ReportReason.valueOf(selectedIDs.get(0));
Bundle args=new Bundle();
@@ -38,6 +110,8 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
args.putParcelable("status", Parcels.wrap(reportStatus));
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", reason.name());
args.putBoolean("fromPost", reportStatus!=null);
args.putParcelable("relationship", Parcels.wrap(relationship));
switch(reason){
case PERSONAL -> {
Nav.go(getActivity(), ReportDoneFragment.class, args);
@@ -48,14 +122,156 @@ public class ReportReasonChoiceFragment extends BaseReportChoiceFragment{
}
}
@Override
protected int getStepNumber(){
return 1;
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
}
@Override
protected void doLoadData(int offset, int count){
}
@Override
protected RecyclerView.Adapter getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
LayoutInflater inflater=getActivity().getLayoutInflater();
View headerView=inflater.inflate(R.layout.item_list_header, list, false);
TextView title=headerView.findViewById(R.id.title);
TextView subtitle=headerView.findViewById(R.id.subtitle);
title.setText(reportStatus!=null ? getString(R.string.report_choose_reason) : getString(R.string.report_choose_reason_account, reportAccount.acct));
subtitle.setText(getString(R.string.report_choose_reason_subtitle));
adapter.addAdapter(new SingleViewRecyclerAdapter(headerView));
adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new ChoiceItemsAdapter(getActivity(), isMultipleChoice, items, list, selectedIDs, btn::setEnabled));
return mergeAdapter=adapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
btn=view.findViewById(R.id.btn_next);
btn.setEnabled(!selectedIDs.isEmpty());
btn.setOnClickListener(v->onButtonClick());
buttonBar=view.findViewById(R.id.button_bar);
progressBar=view.findViewById(R.id.top_progress);
progressBar.setProgress(5);
super.onViewCreated(view, savedInstanceState);
((UsableRecyclerView)list).setIncludeMarginsInItemHitbox(false);
if(reportStatus!=null){
list.addItemDecoration(new RecyclerView.ItemDecoration(){
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
outRect.left=V.dp(16);
outRect.right=V.dp(16);
}
}
});
list.addItemDecoration(new RecyclerView.ItemDecoration(){
private Paint paint=new Paint(Paint.ANTI_ALIAS_FLAG);
{
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(V.dp(1));
paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
}
@Override
public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
int firstPos=list.getChildAdapterPosition(list.getChildAt(0));
int lastPos=-1;
for(int i=list.getChildCount()-1;i>=0;i--){
lastPos=list.getChildAdapterPosition(list.getChildAt(i));
if(lastPos!=-1)
break;
}
int postStart=mergeAdapter.getPositionForAdapter(adapter);
if(lastPos<postStart || firstPos>postStart+displayItems.size()){
return;
}
float top=V.dp(-12);
float bottom=parent.getHeight()+V.dp(12);
for(int i=0;i<parent.getChildCount();i++){
View child=parent.getChildAt(i);
int pos=parent.getChildAdapterPosition(child);
if(pos==postStart)
top=child.getY();
if(pos==postStart+displayItems.size())
bottom=child.getY()-V.dp(16);
}
float off=paint.getStrokeWidth()/2f;
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint);
}
@Override
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
if(holder instanceof StatusDisplayItem.Holder<?>){
outRect.left=outRect.right=V.dp(16);
}
int index=holder.getAbsoluteAdapterPosition()-mergeAdapter.getPositionForAdapter(adapter);
if(index==displayItems.size()){
outRect.top=V.dp(32);
}
}
});
}
}
@Override
protected boolean wantsOverlaySystemNavigation(){
return false;
}
@Override
protected boolean wantsElevationOnScrollEffect(){
return false;
}
@Override
protected List<StatusDisplayItem> buildDisplayItems(Status s){
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER);
}
@Override
protected FilterContext getFilterContext(){
return null;
}
@Override
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){
if((Object)holder instanceof MediaGridStatusDisplayItem.Holder h){
View layout=h.getLayout();
layout.setOutlineProvider(OutlineProviders.roundedRect(8));
layout.setClipToOutline(true);
View overlay=h.getSensitiveOverlay();
overlay.setOutlineProvider(OutlineProviders.roundedRect(8));
overlay.setClipToOutline(true);
}
}
@Override
public void putRelationship(String id, Relationship rel){
super.putRelationship(id, rel);
if(id.equals(reportAccount.id))
relationship=rel;
}
@Override
public Uri getWebUri(Uri.Builder base){
return null;
}
}

View File

@@ -1,6 +1,7 @@
package org.joinmastodon.android.fragments.report;
import android.os.Bundle;
import android.view.View;
import com.squareup.otto.Subscribe;
@@ -14,8 +15,8 @@ import me.grishka.appkit.Nav;
public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
@Override
protected Item getHeaderItem(){
return new Item(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
protected ChoiceItem getHeaderItem(){
return new ChoiceItem(getString(R.string.report_choose_rule), getString(R.string.report_choose_rule_subtitle), null);
}
@Override
@@ -24,7 +25,7 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
Instance inst=AccountSessionManager.getInstance().getInstanceInfo(AccountSessionManager.getInstance().getAccount(accountID).domain);
if(inst!=null && inst.rules!=null){
for(Instance.Rule rule:inst.rules){
items.add(new Item(rule.text, null, rule.id));
items.add(new ChoiceItem(rule.text, null, rule.id));
}
}
}
@@ -37,17 +38,19 @@ public class ReportRuleChoiceFragment extends BaseReportChoiceFragment{
args.putParcelable("reportAccount", Parcels.wrap(reportAccount));
args.putString("reason", getArguments().getString("reason"));
args.putStringArrayList("ruleIDs", selectedIDs);
args.putParcelable("relationship", getArguments().getParcelable("relationship"));
Nav.go(getActivity(), ReportAddPostsChoiceFragment.class, args);
}
@Override
protected int getStepNumber(){
return 1;
}
@Subscribe
public void onFinishReportFragments(FinishReportFragmentsEvent ev){
if(ev.reportAccountID.equals(reportAccount.id))
Nav.finish(this);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
progressBar.setProgress(25);
}
}

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.view.WindowInsets;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.DividerItemDecoration;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.V;
public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<ListItem<T>>{
protected GenericListItemsAdapter<T> itemsAdapter;
protected String accountID;
public BaseSettingsFragment(){
super(20);
}
public BaseSettingsFragment(int perPage){
super(perPage);
}
public BaseSettingsFragment(int layout, int perPage){
super(layout, perPage);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setRetainInstance(true);
accountID=getArguments().getString("account");
setRefreshEnabled(false);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
return itemsAdapter=new GenericListItemsAdapter<T>(data);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof ListItemViewHolder<?> ivh && ivh.getItem().dividerAfter));
list.setItemAnimator(new BetterItemAnimator());
}
protected int indexOfItemsAdapter(){
return 0;
}
protected void toggleCheckableItem(CheckableListItem<T> item){
item.toggle();
rebindItem(item);
}
protected void rebindItem(ListItem<T> item){
if(list==null)
return;
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
holder.rebind();
}
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
list.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
emptyView.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
list.setPadding(0, 0, 0, 0);
}
super.onApplyWindowInsets(insets);
}
}

View File

@@ -0,0 +1,328 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.os.Bundle;
import android.os.Parcelable;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.widget.DatePicker;
import android.widget.EditText;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.api.requests.filters.CreateFilter;
import org.joinmastodon.android.api.requests.filters.DeleteFilter;
import org.joinmastodon.android.api.requests.filters.UpdateFilter;
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.FilterAction;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
public class EditFilterFragment extends BaseSettingsFragment<Void> implements OnBackPressedListener{
private static final int WORDS_RESULT=370;
private static final int CONTEXT_RESULT=651;
private Filter filter;
private ListItem<Void> durationItem, wordsItem, contextItem;
private CheckableListItem<Void> cwItem;
private FloatingHintEditTextLayout titleEditLayout;
private EditText titleEdit;
private Instant endsAt;
private ArrayList<FilterKeyword> keywords=new ArrayList<>();
private ArrayList<String> deletedWordIDs=new ArrayList<>();
private EnumSet<FilterContext> context=EnumSet.allOf(FilterContext.class);
private boolean dirty;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
filter=Parcels.unwrap(getArguments().getParcelable("filter"));
setTitle(filter==null ? R.string.settings_add_filter : R.string.settings_edit_filter);
onDataLoaded(List.of(
durationItem=new ListItem<>(R.string.settings_filter_duration, 0, this::onDurationClick),
wordsItem=new ListItem<>(R.string.settings_filter_muted_words, 0, this::onWordsClick),
contextItem=new ListItem<>(R.string.settings_filter_context, 0, this::onContextClick),
cwItem=new CheckableListItem<>(R.string.settings_filter_show_cw, R.string.settings_filter_show_cw_explanation, CheckableListItem.Style.SWITCH, filter==null || filter.filterAction==FilterAction.WARN, ()->toggleCheckableItem(cwItem))
));
if(filter!=null){
endsAt=filter.expiresAt;
keywords.addAll(filter.keywords);
context=filter.context;
data.add(new ListItem<>(R.string.settings_delete_filter, 0, this::onDeleteClick, R.attr.colorM3Error, false));
}
updateDurationItem();
updateWordsItem();
updateContextItem();
setHasOptionsMenu(true);
setRetainInstance(true);
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, list, false);
titleEdit=titleEditLayout.findViewById(R.id.edit);
titleEdit.setHint(R.string.settings_filter_title);
titleEditLayout.updateHint();
if(filter!=null)
titleEdit.setText(filter.title);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(new SingleViewRecyclerAdapter(titleEditLayout));
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
protected int indexOfItemsAdapter(){
return 1;
}
private void onDurationClick(){
int[] durationOptions={
1800,
3600,
12*3600,
24*3600,
3*24*3600,
7*24*3600
};
ArrayList<String> options=Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).collect(Collectors.toCollection(ArrayList<String>::new));
options.add(0, getString(R.string.filter_duration_forever));
options.add(getString(R.string.filter_duration_custom));
Instant[] newEnd={null};
boolean[] isCustom={false};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_filter_duration_title)
.setSupportingText(endsAt==null ? null : getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false)))
.setSingleChoiceItems(options.toArray(new String[0]), -1, (dlg, item)->{
AlertDialog a=(AlertDialog) dlg;
if(item==options.size()-1){ // custom
showCustomDurationAlert(isCustom[0] ? newEnd[0] : null, date->{
if(date==null){
a.getListView().setItemChecked(item, false);
}else{
isCustom[0]=true;
newEnd[0]=date;
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
});
}else{
isCustom[0]=false;
if(item==0){
newEnd[0]=null;
}else{
newEnd[0]=Instant.now().plusSeconds(durationOptions[item-1]);
}
a.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
})
.setPositiveButton(R.string.ok, (dlg, item)->{
if(!Objects.equals(endsAt, newEnd[0])){
endsAt=newEnd[0];
updateDurationItem();
dirty=true;
}
})
.setNegativeButton(R.string.cancel, null)
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
private void showCustomDurationAlert(Instant currentValue, Consumer<Instant> callback){
DatePicker picker=new DatePicker(getActivity());
picker.setMinDate(LocalDate.now().plusDays(1).atStartOfDay(ZoneId.systemDefault()).toEpochSecond()*1000L);
if(currentValue!=null){
ZonedDateTime dt=currentValue.atZone(ZoneId.systemDefault());
picker.updateDate(dt.getYear(), dt.getMonthValue()-1, dt.getDayOfMonth());
}
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setView(picker)
.setPositiveButton(R.string.ok, (dlg, item)->{
((AlertDialog)dlg).setOnDismissListener(null);
callback.accept(LocalDate.of(picker.getYear(), picker.getMonth()+1, picker.getDayOfMonth()).atStartOfDay(ZoneId.systemDefault()).toInstant());
})
.setNegativeButton(R.string.cancel, null)
.show();
alert.setOnDismissListener(dialog->callback.accept(null));
}
private void onWordsClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) keywords.stream().map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
Nav.goForResult(getActivity(), FilterWordsFragment.class, args, WORDS_RESULT, this);
}
private void onContextClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putSerializable("context", context);
Nav.goForResult(getActivity(), FilterContextFragment.class, args, CONTEXT_RESULT, this);
}
private void onDeleteClick(){
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(getString(R.string.settings_delete_filter_title, filter.title))
.setMessage(R.string.settings_delete_filter_confirmation)
.setPositiveButton(R.string.delete, (dlg, item)->deleteFilter())
.setNegativeButton(R.string.cancel, null)
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
}
private void updateDurationItem(){
if(endsAt==null){
durationItem.subtitle=getString(R.string.filter_duration_forever);
}else{
durationItem.subtitle=getString(R.string.settings_filter_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), endsAt, false));
}
rebindItem(durationItem);
}
private void updateWordsItem(){
wordsItem.subtitle=getResources().getQuantityString(R.plurals.settings_x_muted_words, keywords.size(), keywords.size());
rebindItem(wordsItem);
}
private void updateContextItem(){
List<String> values=context.stream().map(c->getString(c.getDisplayNameRes())).collect(Collectors.toList());
contextItem.subtitle=switch(values.size()){
case 0 -> null;
case 1 -> values.get(0);
case 2 -> getString(R.string.selection_2_options, values.get(0), values.get(1));
case 3 -> getString(R.string.selection_3_options, values.get(0), values.get(1), values.get(2));
default -> getString(R.string.selection_4_or_more, values.get(0), values.get(1), values.size()-2);
};
rebindItem(contextItem);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.settings_edit_filter, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
if(item.getItemId()==R.id.save){
saveFilter();
}
return true;
}
private void saveFilter(){
if(titleEdit.length()==0){
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
return;
}
MastodonAPIRequest<Filter> req;
if(filter==null){
req=new CreateFilter(titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords);
}else{
req=new UpdateFilter(filter.id, titleEdit.getText().toString(), context, cwItem.checked ? FilterAction.WARN : FilterAction.HIDE, endsAt==null ? 0 : (int)(endsAt.getEpochSecond()-Instant.now().getEpochSecond()), keywords, deletedWordIDs);
}
req.setCallback(new Callback<>(){
@Override
public void onSuccess(Filter result){
E.post(new SettingsFilterCreatedOrUpdatedEvent(accountID, result));
Nav.finish(EditFilterFragment.this);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.saving, true)
.exec(accountID);
}
private void deleteFilter(){
new DeleteFilter(filter.id)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Void result){
E.post(new SettingsFilterDeletedEvent(accountID, filter.id));
Nav.finish(EditFilterFragment.this);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getActivity());
}
})
.wrapProgress(getActivity(), R.string.deleting, false)
.exec(accountID);
}
@Override
public void onFragmentResult(int reqCode, boolean success, Bundle result){
if(success){
if(reqCode==CONTEXT_RESULT){
EnumSet<FilterContext> context=(EnumSet<FilterContext>) result.getSerializable("context");
if(!context.equals(this.context)){
this.context=context;
dirty=true;
updateContextItem();
}
}else if(reqCode==WORDS_RESULT){
ArrayList<FilterKeyword> old=new ArrayList<>(keywords);
keywords.clear();
result.getParcelableArrayList("words").stream().map(p->(FilterKeyword)Parcels.unwrap(p)).forEach(keywords::add);
if(!old.equals(keywords)){
dirty=true;
updateWordsItem();
}
deletedWordIDs.addAll(result.getStringArrayList("deleted"));
}
}
}
private boolean isDirty(){
return dirty || (filter!=null && !titleEdit.getText().toString().equals(filter.title)) || (filter!=null && (filter.filterAction==FilterAction.WARN)!=cwItem.checked);
}
@Override
public boolean onBackPressed(){
if(isDirty()){
UiUtils.showConfirmationAlert(getActivity(), R.string.discard_changes, 0, R.string.discard, ()->Nav.finish(this));
return true;
}
return false;
}
}

View File

@@ -0,0 +1,48 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.FilterContext;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.stream.Collectors;
import me.grishka.appkit.fragments.OnBackPressedListener;
public class FilterContextFragment extends BaseSettingsFragment<FilterContext> implements OnBackPressedListener{
private EnumSet<FilterContext> context;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_filter_context);
context=(EnumSet<FilterContext>) getArguments().getSerializable("context");
onDataLoaded(Arrays.stream(FilterContext.values()).map(c->{
CheckableListItem<FilterContext> item=new CheckableListItem<>(c.getDisplayNameRes(), 0, CheckableListItem.Style.CHECKBOX, context.contains(c), null);
item.parentObject=c;
item.isEnabled=true;
item.onClick=()->toggleCheckableItem(item);
return item;
}).collect(Collectors.toList()));
}
@Override
protected void doLoadData(int offset, int count){}
@Override
public boolean onBackPressed(){
context=EnumSet.noneOf(FilterContext.class);
for(ListItem<FilterContext> item:data){
if(((CheckableListItem<FilterContext>) item).checked)
context.add(item.parentObject);
}
Bundle args=new Bundle();
args.putSerializable("context", context);
setResult(true, args);
return false;
}
}

View File

@@ -0,0 +1,327 @@
package org.joinmastodon.android.fragments.settings;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.IntEvaluator;
import android.animation.ObjectAnimator;
import android.app.AlertDialog;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.InputType;
import android.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageButton;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.FilterKeyword;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
import org.parceler.Parcels;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.FragmentStackActivity;
import me.grishka.appkit.fragments.OnBackPressedListener;
import me.grishka.appkit.utils.V;
public class FilterWordsFragment extends BaseSettingsFragment<FilterKeyword> implements OnBackPressedListener{
private ImageButton fab;
private ActionMode actionMode;
private ArrayList<ListItem<FilterKeyword>> selectedItems=new ArrayList<>();
private ArrayList<String> deletedItemIDs=new ArrayList<>();
private MenuItem deleteItem;
public FilterWordsFragment(){
setListLayoutId(R.layout.recycler_fragment_with_fab);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_filter_muted_words);
onDataLoaded(getArguments().getParcelableArrayList("words").stream().map(p->{
FilterKeyword word=Parcels.unwrap(p);
ListItem<FilterKeyword> item=new ListItem<>(word.keyword, null, null, word);
item.isEnabled=true;
item.onClick=()->onWordClick(item);
return item;
}).collect(Collectors.toList()));
setHasOptionsMenu(true);
}
@Override
protected void doLoadData(int offset, int count){}
private void onWordClick(ListItem<FilterKeyword> item){
showAlertForWord(item.parentObject);
}
private void onSelectionModeWordClick(CheckableListItem<FilterKeyword> item){
if(selectedItems.remove(item)){
item.checked=false;
}else{
item.checked=true;
selectedItems.add(item);
}
rebindItem(item);
updateActionModeTitle();
}
@Override
public boolean onBackPressed(){
Bundle result=new Bundle();
result.putParcelableArrayList("words", (ArrayList<? extends Parcelable>) data.stream().map(i->i.parentObject).map(Parcels::wrap).collect(Collectors.toCollection(ArrayList::new)));
result.putStringArrayList("deleted", deletedItemIDs);
setResult(true, result);
return false;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
fab=view.findViewById(R.id.fab);
fab.setImageResource(R.drawable.ic_add_24px);
fab.setContentDescription(getString(R.string.add_muted_word));
fab.setOnClickListener(v->onFabClick());
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
int fabInset=0;
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
fabInset=insets.getSystemWindowInsetBottom();
}
((ViewGroup.MarginLayoutParams) fab.getLayoutParams()).bottomMargin=V.dp(16)+fabInset;
super.onApplyWindowInsets(insets);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
inflater.inflate(R.menu.settings_filter_words, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item){
enterSelectionMode(item.getItemId()==R.id.select_all);
return true;
}
@Override
public boolean wantsLightStatusBar(){
if(actionMode!=null)
return UiUtils.isDarkTheme();
return super.wantsLightStatusBar();
}
private void onFabClick(){
showAlertForWord(null);
}
private void showAlertForWord(FilterKeyword word){
AlertDialog.Builder bldr=new M3AlertDialogBuilder(getActivity())
.setHelpText(R.string.filter_add_word_help)
.setTitle(word==null ? R.string.add_muted_word : R.string.edit_muted_word)
.setNegativeButton(R.string.cancel, null);
FloatingHintEditTextLayout editWrap=(FloatingHintEditTextLayout) bldr.getContext().getSystemService(LayoutInflater.class).inflate(R.layout.floating_hint_edit_text, null);
EditText edit=editWrap.findViewById(R.id.edit);
edit.setHint(R.string.filter_word_or_phrase);
edit.setInputType(InputType.TYPE_TEXT_VARIATION_FILTER);
editWrap.updateHint();
bldr.setView(editWrap)
.setPositiveButton(word==null ? R.string.add : R.string.save, null);
if(word!=null){
edit.setText(word.keyword);
bldr.setNeutralButton(R.string.delete, null);
}
AlertDialog alert=bldr.show();
if(word!=null){
Button deleteBtn=alert.getButton(AlertDialog.BUTTON_NEUTRAL);
deleteBtn.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
deleteBtn.setOnClickListener(v->confirmDeleteWords(Collections.singletonList(word), alert::dismiss));
}
Button saveBtn=alert.getButton(AlertDialog.BUTTON_POSITIVE);
saveBtn.setEnabled(false);
saveBtn.setOnClickListener(v->{
String input=edit.getText().toString();
for(ListItem<FilterKeyword> item:data){
if(item.parentObject.keyword.equalsIgnoreCase(input)){
editWrap.setErrorState(getString(R.string.filter_word_already_in_list));
return;
}
}
if(word==null){
FilterKeyword w=new FilterKeyword();
w.wholeWord=true;
w.keyword=input;
ListItem<FilterKeyword> item=new ListItem<>(w.keyword, null, null, w);
item.isEnabled=true;
item.onClick=()->onWordClick(item);
data.add(item);
itemsAdapter.notifyItemInserted(data.size()-1);
}else{
word.keyword=input;
word.wholeWord=true;
for(ListItem<FilterKeyword> item:data){
if(item.parentObject==word){
rebindItem(item);
break;
}
}
}
alert.dismiss();
});
edit.addTextChangedListener(new SimpleTextWatcher(e->saveBtn.setEnabled(e.length()>0)));
}
private void confirmDeleteWords(List<FilterKeyword> words, Runnable onConfirmed){
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(words.size()==1 ? getString(R.string.settings_delete_filter_word, words.get(0).keyword) : getResources().getQuantityString(R.plurals.settings_delete_x_filter_words, words.size(), words.size()))
// .setMessage(R.string.settings_delete_filter_confirmation)
.setPositiveButton(R.string.delete, (dlg, item)->{
if(onConfirmed!=null)
onConfirmed.run();
removeWords(words);
})
.setNegativeButton(R.string.cancel, null)
.show();
alert.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error));
}
private void removeWords(List<FilterKeyword> words){
ArrayList<Integer> indexes=new ArrayList<>();
for(int i=0;i<data.size();i++){
if(words.contains(data.get(i).parentObject)){
indexes.add(0, i);
}
}
for(int index:indexes){
data.remove(index);
itemsAdapter.notifyItemRemoved(index);
}
for(FilterKeyword w:words){
if(w.id!=null)
deletedItemIDs.add(w.id);
}
}
private void enterSelectionMode(boolean selectAll){
if(actionMode!=null)
return;
V.setVisibilityAnimated(fab, View.GONE);
actionMode=getActivity().startActionMode(new ActionMode.Callback(){
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu){
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", elevationOnScrollListener.getCurrentStatusBarColor(), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
for(int i=0;i<menu.size();i++){
Drawable icon=menu.getItem(i).getIcon().mutate();
icon.setTint(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnPrimary));
menu.getItem(i).setIcon(icon);
}
deleteItem=menu.findItem(R.id.delete);
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
if(item.getItemId()==R.id.delete){
confirmDeleteWords(selectedItems.stream().map(i->i.parentObject).collect(Collectors.toList()), ()->leaveSelectionMode(false));
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode){
leaveSelectionMode(true);
ObjectAnimator anim=ObjectAnimator.ofInt(getActivity().getWindow(), "statusBarColor", UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary), elevationOnScrollListener.getCurrentStatusBarColor());
anim.setEvaluator(new IntEvaluator(){
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue){
return UiUtils.alphaBlendColors(startValue, endValue, fraction);
}
});
anim.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
getActivity().getWindow().setStatusBarColor(0);
}
});
anim.start();
((FragmentStackActivity) getActivity()).invalidateSystemBarColors(FilterWordsFragment.this);
}
});
selectedItems.clear();
for(int i=0;i<data.size();i++){
ListItem<FilterKeyword> item=data.get(i);
CheckableListItem<FilterKeyword> newItem=new CheckableListItem<>(item.title, null, CheckableListItem.Style.CHECKBOX, selectAll, null);
newItem.isEnabled=true;
newItem.onClick=()->onSelectionModeWordClick(newItem);
newItem.parentObject=item.parentObject;
if(selectAll)
selectedItems.add(newItem);
data.set(i, newItem);
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
updateActionModeTitle();
}
private void leaveSelectionMode(boolean fromActionMode){
if(actionMode==null)
return;
ActionMode actionMode=this.actionMode;
this.actionMode=null;
if(!fromActionMode)
actionMode.finish();
V.setVisibilityAnimated(fab, View.VISIBLE);
selectedItems.clear();
for(int i=0;i<data.size();i++){
ListItem<FilterKeyword> item=data.get(i);
ListItem<FilterKeyword> newItem=new ListItem<>(item.title, null, null);
newItem.isEnabled=true;
newItem.onClick=()->onWordClick(newItem);
newItem.parentObject=item.parentObject;
data.set(i, newItem);
}
itemsAdapter.notifyItemRangeChanged(0, data.size());
}
private void updateActionModeTitle(){
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedItems.size(), selectedItems.size()));
deleteItem.setEnabled(!selectedItems.isEmpty());
}
}

View File

@@ -0,0 +1,82 @@
package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.os.Bundle;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.imageloader.ImageCache;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
private ListItem<Void> mediaCacheItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
AccountSession s=AccountSessionManager.get(accountID);
onDataLoaded(List.of(
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.donate_url))),
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/terms")),
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick)
));
updateMediaCacheItem();
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(super.getAdapter());
TextView versionInfo=new TextView(getActivity());
versionInfo.setSingleLine();
versionInfo.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(32)));
versionInfo.setTextAppearance(R.style.m3_label_medium);
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
versionInfo.setGravity(Gravity.CENTER);
versionInfo.setText(getString(R.string.sk_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
return adapter;
}
private void onClearMediaCacheClick(){
MastodonAPIController.runInBackground(()->{
Activity activity=getActivity();
ImageCache.getInstance(getActivity()).clear();
activity.runOnUiThread(()->{
Toast.makeText(activity, R.string.media_cache_cleared, Toast.LENGTH_SHORT).show();
updateMediaCacheItem();
});
});
}
private void updateMediaCacheItem(){
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
mediaCacheItem.isEnabled=size>0;
rebindItem(mediaCacheItem);
}
}

View File

@@ -0,0 +1,192 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import androidx.annotation.StringRes;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.viewcontrollers.ComposeLanguageAlertViewController;
import org.joinmastodon.android.utils.MastodonLanguage;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.IntStream;
public class SettingsBehaviorFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private ListItem<Void> languageItem;
private CheckableListItem<Void> altTextItem, playGifsItem, customTabsItem, confirmUnfollowItem, confirmBoostItem, confirmDeleteItem;
private MastodonLanguage postLanguage;
private ComposeLanguageAlertViewController.SelectedOption newPostLanguage;
// MEGALODON
private MastodonLanguage.LanguageResolver languageResolver;
private ListItem<Void> prefixRepliesItem, replyVisibilityItem;
private CheckableListItem<Void> forwardReportsItem, remoteLoadingItem, showBoostsItem, showRepliesItem, loadNewPostsItem, seeNewPostsBtnItem;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_behavior);
AccountSession s=AccountSessionManager.get(accountID);
AccountLocalPreferences lp=getLocalPrefs();
languageResolver = s.getInstance().map(MastodonLanguage.LanguageResolver::new).orElse(null);
postLanguage=s.preferences==null || s.preferences.postingDefaultLanguage==null ? null :
languageResolver.from(s.preferences.postingDefaultLanguage).orElse(null);
List<ListItem<Void>> items = new ArrayList<>(List.of(
languageItem=new ListItem<>(getString(R.string.default_post_language), postLanguage!=null ? postLanguage.getDisplayName(getContext()) : null, R.drawable.ic_fluent_local_language_24_regular, this::onDefaultLanguageClick),
altTextItem=new CheckableListItem<>(R.string.settings_alt_text_reminders, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.altTextReminders, R.drawable.ic_fluent_image_alt_text_24_regular, ()->toggleCheckableItem(altTextItem)),
playGifsItem=new CheckableListItem<>(R.string.settings_gif, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.playGifs, R.drawable.ic_fluent_gif_24_regular, ()->toggleCheckableItem(playGifsItem)),
customTabsItem=new CheckableListItem<>(R.string.settings_custom_tabs, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.useCustomTabs, R.drawable.ic_fluent_link_24_regular, ()->toggleCheckableItem(customTabsItem)),
confirmUnfollowItem=new CheckableListItem<>(R.string.settings_confirm_unfollow, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmUnfollow, R.drawable.ic_fluent_person_delete_24_regular, ()->toggleCheckableItem(confirmUnfollowItem)),
confirmBoostItem=new CheckableListItem<>(R.string.settings_confirm_boost, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmBoost, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(confirmBoostItem)),
confirmDeleteItem=new CheckableListItem<>(R.string.settings_confirm_delete_post, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.confirmDeletePost, R.drawable.ic_fluent_delete_24_regular, ()->toggleCheckableItem(confirmDeleteItem)),
prefixRepliesItem=new ListItem<>(R.string.sk_settings_prefix_reply_cw_with_re, getPrefixWithRepliesString(), R.drawable.ic_fluent_arrow_reply_24_regular, this::onPrefixRepliesClick),
forwardReportsItem=new CheckableListItem<>(R.string.sk_settings_forward_report_default, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.forwardReportDefault, R.drawable.ic_fluent_arrow_forward_24_regular, ()->toggleCheckableItem(forwardReportsItem)),
loadNewPostsItem=new CheckableListItem<>(R.string.sk_settings_load_new_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.loadNewPosts, R.drawable.ic_fluent_arrow_sync_24_regular, this::onLoadNewPostsClick),
seeNewPostsBtnItem=new CheckableListItem<>(R.string.sk_settings_see_new_posts_button, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNewPostsButton, R.drawable.ic_fluent_arrow_up_24_regular, ()->toggleCheckableItem(seeNewPostsBtnItem)),
remoteLoadingItem=new CheckableListItem<>(R.string.sk_settings_allow_remote_loading, R.string.sk_settings_allow_remote_loading_explanation, CheckableListItem.Style.SWITCH, GlobalUserPreferences.allowRemoteLoading, R.drawable.ic_fluent_communication_24_regular, ()->toggleCheckableItem(remoteLoadingItem), true),
showBoostsItem=new CheckableListItem<>(R.string.sk_settings_show_boosts, 0, CheckableListItem.Style.SWITCH, lp.showBoosts, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(showBoostsItem)),
showRepliesItem=new CheckableListItem<>(R.string.sk_settings_show_replies, 0, CheckableListItem.Style.SWITCH, lp.showReplies, R.drawable.ic_fluent_arrow_reply_24_regular, ()->toggleCheckableItem(showRepliesItem))
));
if(isInstanceAkkoma()) items.add(
replyVisibilityItem=new ListItem<>(R.string.sk_settings_reply_visibility, getReplyVisibilityString(), R.drawable.ic_fluent_chat_24_regular, this::onReplyVisibilityClick)
);
loadNewPostsItem.checkedChangeListener=checked->onLoadNewPostsClick();
seeNewPostsBtnItem.isEnabled=loadNewPostsItem.checked;
onDataLoaded(items);
}
private @StringRes int getPrefixWithRepliesString(){
return switch(GlobalUserPreferences.prefixReplies){
case NEVER -> R.string.sk_settings_prefix_replies_never;
case ALWAYS -> R.string.sk_settings_prefix_replies_always;
case TO_OTHERS -> R.string.sk_settings_prefix_replies_to_others;
};
}
private @StringRes int getReplyVisibilityString(){
AccountLocalPreferences lp=getLocalPrefs();
if (lp.timelineReplyVisibility==null) return R.string.sk_settings_reply_visibility_all;
return switch(lp.timelineReplyVisibility){
case "following" -> R.string.sk_settings_reply_visibility_following;
case "self" -> R.string.sk_settings_reply_visibility_self;
default -> R.string.sk_settings_reply_visibility_all;
};
}
@Override
protected void doLoadData(int offset, int count){}
private void onDefaultLanguageClick(){
if (languageResolver == null) return;
ComposeLanguageAlertViewController vc=new ComposeLanguageAlertViewController(getActivity(), null, new ComposeLanguageAlertViewController.SelectedOption(postLanguage), null, languageResolver);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.default_post_language)
.setView(vc.getView())
.setPositiveButton(R.string.ok, (dlg, which)->{
ComposeLanguageAlertViewController.SelectedOption opt=vc.getSelectedOption();
if(!opt.language.equals(postLanguage)){
newPostLanguage=opt;
postLanguage=newPostLanguage.language;
languageItem.subtitle=newPostLanguage.language.getDefaultName();
rebindItem(languageItem);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onPrefixRepliesClick(){
int selected=GlobalUserPreferences.prefixReplies.ordinal();
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_prefix_reply_cw_with_re)
.setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_prefix_replies_never, R.string.sk_settings_prefix_replies_always, R.string.sk_settings_prefix_replies_to_others).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.prefixReplies=GlobalUserPreferences.PrefixRepliesMode.values()[newSelected[0]];
prefixRepliesItem.subtitleRes=getPrefixWithRepliesString();
rebindItem(prefixRepliesItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onReplyVisibilityClick(){
AccountLocalPreferences lp=getLocalPrefs();
int selected=lp.timelineReplyVisibility==null ? 2 : switch(lp.timelineReplyVisibility){
case "following" -> 0;
case "self" -> 1;
default -> 2;
};
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_prefix_reply_cw_with_re)
.setSingleChoiceItems((String[]) IntStream.of(R.string.sk_settings_reply_visibility_following, R.string.sk_settings_reply_visibility_self, R.string.sk_settings_reply_visibility_all).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
lp.timelineReplyVisibility=switch(newSelected[0]){
case 0 -> "following";
case 1 -> "self";
default -> null;
};
replyVisibilityItem.subtitleRes=getReplyVisibilityString();
rebindItem(replyVisibilityItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onLoadNewPostsClick(){
toggleCheckableItem(loadNewPostsItem);
seeNewPostsBtnItem.checked=loadNewPostsItem.checked;
seeNewPostsBtnItem.isEnabled=loadNewPostsItem.checked;
rebindItem(seeNewPostsBtnItem);
}
@Override
protected void onHidden(){
super.onHidden();
GlobalUserPreferences.playGifs=playGifsItem.checked;
GlobalUserPreferences.useCustomTabs=customTabsItem.checked;
GlobalUserPreferences.altTextReminders=altTextItem.checked;
GlobalUserPreferences.confirmUnfollow=customTabsItem.checked;
GlobalUserPreferences.confirmBoost=confirmBoostItem.checked;
GlobalUserPreferences.confirmDeletePost=confirmDeleteItem.checked;
GlobalUserPreferences.forwardReportDefault=forwardReportsItem.checked;
GlobalUserPreferences.loadNewPosts=loadNewPostsItem.checked;
GlobalUserPreferences.showNewPostsButton=seeNewPostsBtnItem.checked;
GlobalUserPreferences.allowRemoteLoading=remoteLoadingItem.checked;
GlobalUserPreferences.save();
AccountLocalPreferences lp=getLocalPrefs();
lp.showBoosts=showBoostsItem.checked;
lp.showReplies=showRepliesItem.checked;
lp.save();
if(newPostLanguage!=null){
AccountSession s=AccountSessionManager.get(accountID);
if(s.preferences==null)
s.preferences=new Preferences();
s.preferences.postingDefaultLanguage=newPostLanguage.language.getLanguage();
s.savePreferencesLater();
}
}
@Override
public String getAccountID(){
return accountID;
}
}

View File

@@ -0,0 +1,73 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.session.AccountActivationInfo;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.onboarding.AccountActivationFragment;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.List;
import me.grishka.appkit.Nav;
public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle("Debug settings");
ListItem<Void> selfUpdateItem, resetUpdateItem;
onDataLoaded(List.of(
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)
));
if(!GithubSelfUpdater.needSelfUpdating()){
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
selfUpdateItem.subtitle="Self-updater is unavailable in this build flavor";
}
}
@Override
protected void doLoadData(int offset, int count){}
private void onTestEmailConfirmClick(){
AccountSession sess=AccountSessionManager.getInstance().getAccount(accountID);
sess.activated=false;
sess.activationInfo=new AccountActivationInfo("test@email", System.currentTimeMillis());
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("debug", true);
Nav.goClearingStack(getActivity(), AccountActivationFragment.class, args);
}
private void onForceSelfUpdateClick(){
GithubSelfUpdater.forceUpdate=true;
GithubSelfUpdater.getInstance().maybeCheckForUpdates();
restartUI();
}
private void onResetUpdaterClick(){
GithubSelfUpdater.getInstance().reset();
restartUI();
}
private void onResetDiscoverBannersClick(){
DiscoverInfoBannerHelper.reset();
restartUI();
}
private void restartUI(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.goClearingStack(getActivity(), HomeFragment.class, args);
}
}

View File

@@ -0,0 +1,293 @@
package org.joinmastodon.android.fragments.settings;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import androidx.annotation.StringRes;
import org.joinmastodon.android.E;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import me.grishka.appkit.FragmentStackActivity;
public class SettingsDisplayFragment extends BaseSettingsFragment<Void>{
private ImageView themeTransitionWindowView;
private ListItem<Void> themeItem;
private CheckableListItem<Void> revealCWsItem, hideSensitiveMediaItem, interactionCountsItem, emojiInNamesItem;
// MEGALODON
private CheckableListItem<Void> trueBlackModeItem, marqueeItem, disableSwipeItem, reduceMotionItem, altIndicatorItem, noAltIndicatorItem, collapsePostsItem, spectatorModeItem, hideFabItem, translateOpenedItem, disablePillItem;
private ListItem<Void> colorItem, publishTextItem, autoRevealCWsItem;
private AccountLocalPreferences lp;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_display);
AccountSession s=AccountSessionManager.get(accountID);
lp=s.getLocalPreferences();
onDataLoaded(List.of(
themeItem=new ListItem<>(R.string.settings_theme, getAppearanceValue(), R.drawable.ic_fluent_weather_moon_24_regular, this::onAppearanceClick),
colorItem=new ListItem<>(R.string.sk_settings_color_palette, getColorPaletteValue(), R.drawable.ic_fluent_color_24_regular, this::onColorClick),
trueBlackModeItem=new CheckableListItem<>(R.string.sk_settings_true_black, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.trueBlackTheme, R.drawable.ic_fluent_dark_theme_24_regular, this::onTrueBlackModeClick, true),
publishTextItem=new ListItem<>(getString(R.string.sk_settings_publish_button_text), getPublishButtonText(), R.drawable.ic_fluent_send_24_regular, this::onPublishTextClick),
autoRevealCWsItem=new ListItem<>(R.string.sk_settings_auto_reveal_equal_spoilers, getAutoRevealSpoilersText(), R.drawable.ic_fluent_eye_24_regular, this::onAutoRevealSpoilersClick),
revealCWsItem=new CheckableListItem<>(R.string.sk_settings_always_reveal_content_warnings, 0, CheckableListItem.Style.SWITCH, lp.revealCWs, R.drawable.ic_fluent_chat_warning_24_regular, ()->toggleCheckableItem(revealCWsItem)),
hideSensitiveMediaItem=new CheckableListItem<>(R.string.settings_hide_sensitive_media, 0, CheckableListItem.Style.SWITCH, lp.hideSensitiveMedia, R.drawable.ic_fluent_flag_24_regular, ()->toggleCheckableItem(hideSensitiveMediaItem)),
interactionCountsItem=new CheckableListItem<>(R.string.settings_show_interaction_counts, 0, CheckableListItem.Style.SWITCH, lp.showInteractionCounts, R.drawable.ic_fluent_number_row_24_regular, ()->toggleCheckableItem(interactionCountsItem)),
emojiInNamesItem=new CheckableListItem<>(R.string.settings_show_emoji_in_names, 0, CheckableListItem.Style.SWITCH, lp.customEmojiInNames, R.drawable.ic_fluent_emoji_24_regular, ()->toggleCheckableItem(emojiInNamesItem)),
marqueeItem=new CheckableListItem<>(R.string.sk_settings_enable_marquee, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.toolbarMarquee, R.drawable.ic_fluent_text_more_24_regular, ()->toggleCheckableItem(marqueeItem)),
reduceMotionItem=new CheckableListItem<>(R.string.sk_settings_reduce_motion, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.reduceMotion, R.drawable.ic_fluent_star_emphasis_24_regular, ()->toggleCheckableItem(reduceMotionItem)),
disableSwipeItem=new CheckableListItem<>(R.string.sk_settings_tabs_disable_swipe, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableSwipe, R.drawable.ic_fluent_swipe_right_24_regular, ()->toggleCheckableItem(disableSwipeItem)),
altIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showAltIndicator, R.drawable.ic_fluent_scan_text_24_regular, ()->toggleCheckableItem(altIndicatorItem)),
noAltIndicatorItem=new CheckableListItem<>(R.string.sk_settings_show_no_alt_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.showNoAltIndicator, R.drawable.ic_fluent_important_24_regular, ()->toggleCheckableItem(noAltIndicatorItem)),
collapsePostsItem=new CheckableListItem<>(R.string.sk_settings_collapse_long_posts, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.collapseLongPosts, R.drawable.ic_fluent_chevron_down_24_regular, ()->toggleCheckableItem(collapsePostsItem)),
spectatorModeItem=new CheckableListItem<>(R.string.sk_settings_hide_interaction, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.spectatorMode, R.drawable.ic_fluent_star_off_24_regular, ()->toggleCheckableItem(spectatorModeItem)),
hideFabItem=new CheckableListItem<>(R.string.sk_settings_hide_fab, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.autoHideFab, R.drawable.ic_fluent_edit_24_regular, ()->toggleCheckableItem(hideFabItem)),
translateOpenedItem=new CheckableListItem<>(R.string.sk_settings_translate_only_opened, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.translateButtonOpenedOnly, R.drawable.ic_fluent_translate_24_regular, ()->toggleCheckableItem(translateOpenedItem)),
disablePillItem=new CheckableListItem<>(R.string.sk_disable_pill_shaped_active_indicator, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.disableM3PillActiveIndicator, R.drawable.ic_fluent_pill_24_regular, ()->toggleCheckableItem(disablePillItem))
));
trueBlackModeItem.checkedChangeListener=checked->onTrueBlackModeClick();
}
@Override
protected void doLoadData(int offset, int count){}
@Override
public void onAttach(Activity activity){
super.onAttach(activity);
if(themeTransitionWindowView!=null){
// Activity has finished recreating. Remove the overlay.
activity.getSystemService(WindowManager.class).removeView(themeTransitionWindowView);
themeTransitionWindowView=null;
}
}
@Override
protected void onHidden(){
super.onHidden();
boolean restartPlease=
GlobalUserPreferences.disableM3PillActiveIndicator!=disablePillItem.checked;
lp.revealCWs=revealCWsItem.checked;
lp.hideSensitiveMedia=hideSensitiveMediaItem.checked;
lp.showInteractionCounts=interactionCountsItem.checked;
lp.customEmojiInNames=emojiInNamesItem.checked;
lp.save();
GlobalUserPreferences.toolbarMarquee=marqueeItem.checked;
GlobalUserPreferences.reduceMotion=reduceMotionItem.checked;
GlobalUserPreferences.disableSwipe=disableSwipeItem.checked;
GlobalUserPreferences.showAltIndicator=altIndicatorItem.checked;
GlobalUserPreferences.showNoAltIndicator=noAltIndicatorItem.checked;
GlobalUserPreferences.collapseLongPosts=collapsePostsItem.checked;
GlobalUserPreferences.spectatorMode=spectatorModeItem.checked;
GlobalUserPreferences.autoHideFab=hideFabItem.checked;
GlobalUserPreferences.translateButtonOpenedOnly=translateOpenedItem.checked;
GlobalUserPreferences.disableM3PillActiveIndicator=disablePillItem.checked;
GlobalUserPreferences.save();
if(restartPlease) restartActivityToApplyNewTheme();
else E.post(new StatusDisplaySettingsChangedEvent(accountID));
}
private @StringRes int getAppearanceValue(){
return switch(GlobalUserPreferences.theme){
case AUTO -> R.string.theme_auto;
case LIGHT -> R.string.theme_light;
case DARK -> R.string.theme_dark;
};
}
private @StringRes int getColorPaletteValue(){
return switch(GlobalUserPreferences.color){
case MATERIAL3 -> R.string.sk_color_palette_material3;
case PINK -> R.string.sk_color_palette_pink;
case PURPLE -> R.string.sk_color_palette_purple;
case GREEN -> R.string.sk_color_palette_green;
case BLUE -> R.string.sk_color_palette_blue;
case BROWN -> R.string.sk_color_palette_brown;
case RED -> R.string.sk_color_palette_red;
case YELLOW -> R.string.sk_color_palette_yellow;
};
}
private String getPublishButtonText() {
return TextUtils.isEmpty(AccountSessionManager.get(accountID).getLocalPreferences().publishButtonText)
? getContext().getString(R.string.publish)
: AccountSessionManager.get(accountID).getLocalPreferences().publishButtonText;
}
private @StringRes int getAutoRevealSpoilersText() {
return switch(GlobalUserPreferences.autoRevealEqualSpoilers){
case THREADS -> R.string.sk_settings_auto_reveal_author;
case DISCUSSIONS -> R.string.sk_settings_auto_reveal_anyone;
default -> R.string.sk_settings_auto_reveal_nobody;
};
}
private void onTrueBlackModeClick(){
toggleCheckableItem(trueBlackModeItem);
boolean prev=GlobalUserPreferences.trueBlackTheme;
GlobalUserPreferences.trueBlackTheme=trueBlackModeItem.checked;
maybeApplyNewThemeRightNow(null, null, prev);
}
private void onAppearanceClick(){
int selected=switch(GlobalUserPreferences.theme){
case LIGHT -> 0;
case DARK -> 1;
case AUTO -> 2;
};
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setSingleChoiceItems((String[])IntStream.of(R.string.theme_light, R.string.theme_dark, R.string.theme_auto).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.ThemePreference pref=switch(newSelected[0]){
case 0 -> GlobalUserPreferences.ThemePreference.LIGHT;
case 1 -> GlobalUserPreferences.ThemePreference.DARK;
case 2 -> GlobalUserPreferences.ThemePreference.AUTO;
default -> throw new IllegalStateException("Unexpected value: "+newSelected[0]);
};
if(pref!=GlobalUserPreferences.theme){
GlobalUserPreferences.ThemePreference prev=GlobalUserPreferences.theme;
GlobalUserPreferences.theme=pref;
GlobalUserPreferences.save();
themeItem.subtitleRes=getAppearanceValue();
rebindItem(themeItem);
maybeApplyNewThemeRightNow(prev, null, null);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onColorClick(){
int selected=GlobalUserPreferences.color.ordinal();
int[] newSelected={selected};
String[] names=Arrays.stream(GlobalUserPreferences.ColorPreference.values()).map(GlobalUserPreferences.ColorPreference::getName).map(this::getString).toArray(String[]::new);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setSingleChoiceItems(names,
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.ColorPreference pref=GlobalUserPreferences.ColorPreference.values()[newSelected[0]];
if(pref!=GlobalUserPreferences.color){
GlobalUserPreferences.ColorPreference prev=GlobalUserPreferences.color;
GlobalUserPreferences.color=pref;
GlobalUserPreferences.save();
colorItem.subtitleRes=getColorPaletteValue();
rebindItem(colorItem);
maybeApplyNewThemeRightNow(null, prev, null);
}
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onPublishTextClick(){
TextInputFrameLayout input = new TextInputFrameLayout(
getContext(),
getString(R.string.publish),
TextUtils.isEmpty(lp.publishButtonText) ? "" : lp.publishButtonText.trim()
);
new M3AlertDialogBuilder(getContext()).setTitle(R.string.sk_settings_publish_button_text_title).setView(input)
.setPositiveButton(R.string.save, (d, which) -> {
lp.publishButtonText=input.getEditText().getText().toString().trim();
lp.save();
publishTextItem.subtitle=getPublishButtonText();
rebindItem(publishTextItem);
})
.setNeutralButton(R.string.clear, (d, which) -> {
lp.publishButtonText=null;
lp.save();
publishTextItem.subtitle=getPublishButtonText();
rebindItem(publishTextItem);
})
.setNegativeButton(R.string.cancel, (d, which) -> {})
.show();
}
private void onAutoRevealSpoilersClick(){
int selected=GlobalUserPreferences.autoRevealEqualSpoilers.ordinal();
int[] newSelected={selected};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.sk_settings_auto_reveal_equal_spoilers)
.setSingleChoiceItems((String[])IntStream.of(R.string.sk_settings_auto_reveal_nobody, R.string.sk_settings_auto_reveal_author, R.string.sk_settings_auto_reveal_anyone).mapToObj(this::getString).toArray(String[]::new),
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
GlobalUserPreferences.autoRevealEqualSpoilers=GlobalUserPreferences.AutoRevealMode.values()[newSelected[0]];
autoRevealCWsItem.subtitleRes=getAutoRevealSpoilersText();
rebindItem(autoRevealCWsItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void maybeApplyNewThemeRightNow(GlobalUserPreferences.ThemePreference prevTheme, GlobalUserPreferences.ColorPreference prevColor, Boolean prevTrueBlack){
if(prevTheme==null) prevTheme=GlobalUserPreferences.theme;
if(prevTrueBlack==null) prevTrueBlack=GlobalUserPreferences.trueBlackTheme;
if(prevColor==null) prevColor=GlobalUserPreferences.color;
boolean isCurrentDark=prevTheme==GlobalUserPreferences.ThemePreference.DARK ||
(prevTheme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewDark=GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.DARK ||
(GlobalUserPreferences.theme==GlobalUserPreferences.ThemePreference.AUTO && Build.VERSION.SDK_INT>=30 && getResources().getConfiguration().isNightModeActive());
boolean isNewBlack=GlobalUserPreferences.trueBlackTheme;
if(isCurrentDark!=isNewDark || prevColor!=GlobalUserPreferences.color || (isNewDark && prevTrueBlack!=isNewBlack)){
restartActivityToApplyNewTheme();
}
}
private void restartActivityToApplyNewTheme(){
// Calling activity.recreate() causes a black screen for like half a second.
// So, let's take a screenshot and overlay it on top to create the illusion of a smoother transition.
// As a bonus, we can fade it out to make it even smoother.
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N && Build.VERSION.SDK_INT<Build.VERSION_CODES.S){
View activityDecorView=getActivity().getWindow().getDecorView();
Bitmap bitmap=Bitmap.createBitmap(activityDecorView.getWidth(), activityDecorView.getHeight(), Bitmap.Config.ARGB_8888);
activityDecorView.draw(new Canvas(bitmap));
themeTransitionWindowView=new ImageView(MastodonApp.context);
themeTransitionWindowView.setImageBitmap(bitmap);
WindowManager.LayoutParams lp=new WindowManager.LayoutParams(WindowManager.LayoutParams.TYPE_APPLICATION);
lp.flags=WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
lp.systemUiVisibility=View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
lp.systemUiVisibility|=(activityDecorView.getWindowSystemUiVisibility() & (View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR));
lp.width=lp.height=WindowManager.LayoutParams.MATCH_PARENT;
lp.token=getActivity().getWindow().getAttributes().token;
lp.windowAnimations=R.style.window_fade_out;
MastodonApp.context.getSystemService(WindowManager.class).addView(themeTransitionWindowView, lp);
}
getActivity().recreate();
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
}
}

View File

@@ -0,0 +1,112 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.filters.GetFilters;
import org.joinmastodon.android.events.SettingsFilterCreatedOrUpdatedEvent;
import org.joinmastodon.android.events.SettingsFilterDeletedEvent;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
import org.parceler.Parcels;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class SettingsFiltersFragment extends BaseSettingsFragment<Filter>{
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_filters);
loadData();
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected void doLoadData(int offset, int count){
new GetFilters()
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Filter> result){
onDataLoaded(result.stream().map(f->makeListItem(f)).collect(Collectors.toList()));
}
})
.exec(accountID);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(super.getAdapter());
adapter.addAdapter(new GenericListItemsAdapter<>(Collections.singletonList(
new ListItem<Void>(R.string.settings_add_filter, 0, R.drawable.ic_add_24px, this::onAddFilterClick)
)));
return adapter;
}
private void onFilterClick(ListItem<Filter> filter){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("filter", Parcels.wrap(filter.parentObject));
Nav.go(getActivity(), EditFilterFragment.class, args);
}
private void onAddFilterClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), EditFilterFragment.class, args);
}
private ListItem<Filter> makeListItem(Filter f){
ListItem<Filter> item=new ListItem<>(f.title, getString(f.isActive() ? R.string.filter_active : R.string.filter_inactive), null, f);
item.onClick=()->onFilterClick(item);
item.isEnabled=true;
return item;
}
@Subscribe
public void onFilterDeleted(SettingsFilterDeletedEvent ev){
if(!ev.accountID.equals(accountID))
return;
for(int i=0;i<data.size();i++){
if(data.get(i).parentObject.id.equals(ev.filterID)){
data.remove(i);
itemsAdapter.notifyItemRemoved(i);
break;
}
}
}
@Subscribe
public void onFilterCreatedOrUpdated(SettingsFilterCreatedOrUpdatedEvent ev){
if(!ev.accountID.equals(accountID))
return;
for(ListItem<Filter> item:data){
if(item.parentObject.id.equals(ev.filter.id)){
item.parentObject=ev.filter;
item.title=ev.filter.title;
item.subtitle=getString(ev.filter.isActive() ? R.string.filter_active : R.string.filter_inactive);
rebindItem(item);
return;
}
}
data.add(makeListItem(ev.filter));
itemsAdapter.notifyItemInserted(data.size()-1);
}
}

View File

@@ -0,0 +1,115 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HasAccountID;
import org.joinmastodon.android.model.ContentType;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.util.Arrays;
import java.util.List;
import me.grishka.appkit.Nav;
public class SettingsInstanceFragment extends BaseSettingsFragment<Void> implements HasAccountID{
private CheckableListItem<Void> contentTypesItem, localOnlyItem, glitchModeItem;
private ListItem<Void> defaultContentTypeItem;
private AccountLocalPreferences lp;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.sk_settings_instance);
AccountSession s=AccountSessionManager.get(accountID);
lp=s.getLocalPreferences();
onDataLoaded(List.of(
new ListItem<>(AccountSessionManager.get(accountID).domain, getString(R.string.settings_server_explanation), R.drawable.ic_fluent_server_24_regular, this::onServerClick),
new ListItem<>(R.string.sk_settings_profile, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/profile")),
new ListItem<>(R.string.sk_settings_posting, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/settings/preferences/other")),
new ListItem<>(R.string.sk_settings_auth, 0, R.drawable.ic_fluent_open_24_regular, ()->UiUtils.launchWebBrowser(getActivity(), "https://"+s.domain+"/auth/edit"), 0, true),
contentTypesItem=new CheckableListItem<>(R.string.sk_settings_content_types, R.string.sk_settings_content_types_explanation, CheckableListItem.Style.SWITCH, lp.contentTypesEnabled, R.drawable.ic_fluent_text_edit_style_24_regular, this::onContentTypeClick),
defaultContentTypeItem=new ListItem<>(R.string.sk_settings_default_content_type, lp.defaultContentType.getName(), R.drawable.ic_fluent_text_bold_24_regular, this::onDefaultContentTypeClick),
localOnlyItem=new CheckableListItem<>(R.string.sk_settings_support_local_only, R.string.sk_settings_local_only_explanation, CheckableListItem.Style.SWITCH, lp.localOnlySupported, R.drawable.ic_fluent_eye_24_regular, this::onLocalOnlyClick),
glitchModeItem=new CheckableListItem<>(R.string.sk_settings_glitch_instance, R.string.sk_settings_glitch_mode_explanation, CheckableListItem.Style.SWITCH, lp.glitchInstance, R.drawable.ic_fluent_eye_24_filled, ()->toggleCheckableItem(glitchModeItem))
));
contentTypesItem.checkedChangeListener=checked->onContentTypeClick();
defaultContentTypeItem.isEnabled=contentTypesItem.checked;
localOnlyItem.checkedChangeListener=checked->onLocalOnlyClick();
glitchModeItem.isEnabled=localOnlyItem.checked;
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void onHidden(){
super.onHidden();
lp.contentTypesEnabled=contentTypesItem.checked;
lp.localOnlySupported=localOnlyItem.checked;
lp.glitchInstance=glitchModeItem.checked;
lp.save();
}
private void onServerClick(){
Bundle args=new Bundle();
args.putString("account", accountID);
Nav.go(getActivity(), SettingsServerFragment.class, args);
}
private void onContentTypeClick(){
toggleCheckableItem(contentTypesItem);
defaultContentTypeItem.isEnabled=contentTypesItem.checked;
resetDefaultContentType();
rebindItem(defaultContentTypeItem);
}
private void resetDefaultContentType(){
lp.defaultContentType=defaultContentTypeItem.isEnabled
? ContentType.PLAIN : ContentType.UNSPECIFIED;
defaultContentTypeItem.subtitleRes=lp.defaultContentType.getName();
}
private void onDefaultContentTypeClick(){
int selected=lp.defaultContentType.ordinal();
int[] newSelected={selected};
ContentType[] supportedContentTypes=Arrays.stream(ContentType.values())
.filter(t->t.supportedByInstance(getInstance().orElse(null)))
.toArray(ContentType[]::new);
String[] names=Arrays.stream(supportedContentTypes)
.map(ContentType::getName)
.map(this::getString)
.toArray(String[]::new);
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_theme)
.setSingleChoiceItems(names,
selected, (dlg, item)->newSelected[0]=item)
.setPositiveButton(R.string.ok, (dlg, item)->{
ContentType type=supportedContentTypes[newSelected[0]];
lp.defaultContentType=type;
defaultContentTypeItem.subtitleRes=type.getName();
rebindItem(defaultContentTypeItem);
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void onLocalOnlyClick(){
toggleCheckableItem(localOnlyItem);
glitchModeItem.checked=localOnlyItem.checked && !isInstanceAkkoma();
glitchModeItem.isEnabled=localOnlyItem.checked;
rebindItem(glitchModeItem);
}
@Override
public String getAccountID(){
return accountID;
}
}

View File

@@ -0,0 +1,202 @@
package org.joinmastodon.android.fragments.settings;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.otto.Subscribe;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MainActivity;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.updater.GithubSelfUpdater;
import java.util.List;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
public class SettingsMainFragment extends BaseSettingsFragment<Void>{
private boolean loggedOut;
private HideableSingleViewRecyclerAdapter bannerAdapter;
private Button updateButton1, updateButton2;
private TextView updateText;
private Runnable updateDownloadProgressUpdater=new Runnable(){
@Override
public void run(){
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
updateButton1.setText(getString(R.string.downloading_update, Math.round(GithubSelfUpdater.getInstance().getDownloadProgress()*100f)));
list.postDelayed(this, 250);
}
}
};
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings);
setSubtitle(AccountSessionManager.get(accountID).getFullUsername());
onDataLoaded(List.of(
new ListItem<>(R.string.settings_behavior, 0, R.drawable.ic_fluent_settings_24_regular, this::onBehaviorClick),
new ListItem<>(R.string.settings_display, 0, R.drawable.ic_fluent_color_24_regular, this::onDisplayClick),
new ListItem<>(R.string.settings_filters, 0, R.drawable.ic_fluent_filter_24_regular, this::onFiltersClick),
new ListItem<>(R.string.settings_notifications, 0, R.drawable.ic_fluent_alert_24_regular, this::onNotificationsClick),
new ListItem<>(R.string.sk_settings_instance, 0, R.drawable.ic_fluent_server_24_regular, this::onInstanceClick),
new ListItem<>(getString(R.string.about_app, getString(R.string.sk_app_name)), null, R.drawable.ic_fluent_info_24_regular, this::onAboutClick, null, 0, true),
new ListItem<>(R.string.log_out, 0, R.drawable.ic_fluent_sign_out_24_regular, this::onLogOutClick, R.attr.colorM3Error, false)
));
if(BuildConfig.DEBUG || BuildConfig.BUILD_TYPE.equals("appcenterPrivateBeta")){
data.add(0, new ListItem<>("Debug settings", null, R.drawable.ic_fluent_wrench_screwdriver_24_regular, ()->Nav.go(getActivity(), SettingsDebugFragment.class, makeFragmentArgs()), null, 0, true));
}
AccountSessionManager.get(accountID).reloadPreferences(null);
E.register(this);
}
@Override
public void onDestroy(){
super.onDestroy();
E.unregister(this);
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void onHidden(){
super.onHidden();
if(!loggedOut)
AccountSessionManager.get(accountID).savePreferencesIfPending();
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
updateText=banner.findViewById(R.id.text);
TextView bannerTitle=banner.findViewById(R.id.title);
ImageView bannerIcon=banner.findViewById(R.id.icon);
updateButton1=banner.findViewById(R.id.button);
updateButton2=banner.findViewById(R.id.button2);
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
bannerAdapter.setVisible(false);
updateButton1.setOnClickListener(this::onUpdateButtonClick);
updateButton2.setOnClickListener(this::onUpdateButtonClick);
bannerTitle.setText(R.string.app_update_ready);
bannerIcon.setImageResource(R.drawable.ic_fluent_phone_update_24_regular);
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
adapter.addAdapter(bannerAdapter);
adapter.addAdapter(super.getAdapter());
return adapter;
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
if(GithubSelfUpdater.needSelfUpdating()){
updateUpdateBanner();
}
}
private Bundle makeFragmentArgs(){
Bundle args=new Bundle();
args.putString("account", accountID);
return args;
}
private void onBehaviorClick(){
Nav.go(getActivity(), SettingsBehaviorFragment.class, makeFragmentArgs());
}
private void onDisplayClick(){
Nav.go(getActivity(), SettingsDisplayFragment.class, makeFragmentArgs());
}
private void onFiltersClick(){
Nav.go(getActivity(), SettingsFiltersFragment.class, makeFragmentArgs());
}
private void onNotificationsClick(){
Nav.go(getActivity(), SettingsNotificationsFragment.class, makeFragmentArgs());
}
private void onInstanceClick(){
Nav.go(getActivity(), SettingsInstanceFragment.class, makeFragmentArgs());
}
private void onAboutClick(){
Nav.go(getActivity(), SettingsAboutAppFragment.class, makeFragmentArgs());
}
private void onLogOutClick(){
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
new M3AlertDialogBuilder(getActivity())
.setMessage(getString(R.string.confirm_log_out, session.getFullUsername()))
.setPositiveButton(R.string.log_out, (dialog, which)->AccountSessionManager.get(accountID).logOut(getActivity(), ()->{
loggedOut=true;
getActivity().finish();
Intent intent=new Intent(getActivity(), MainActivity.class);
startActivity(intent);
}))
.setNegativeButton(R.string.cancel, null)
.show();
}
@Subscribe
public void onSelfUpdateStateChanged(SelfUpdateStateChangedEvent ev){
updateUpdateBanner();
}
private void updateUpdateBanner(){
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
if(state==GithubSelfUpdater.UpdateState.NO_UPDATE || state==GithubSelfUpdater.UpdateState.CHECKING){
bannerAdapter.setVisible(false);
}else{
bannerAdapter.setVisible(true);
updateText.setText(getString(R.string.app_update_version, GithubSelfUpdater.getInstance().getUpdateInfo().version));
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
updateButton2.setVisibility(View.GONE);
updateButton1.setEnabled(true);
updateButton1.setText(getString(R.string.download_update, UiUtils.formatFileSize(getActivity(), GithubSelfUpdater.getInstance().getUpdateInfo().size, true)));
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADING){
updateButton2.setVisibility(View.VISIBLE);
updateButton2.setText(R.string.cancel);
updateButton1.setEnabled(false);
list.removeCallbacks(updateDownloadProgressUpdater);
updateDownloadProgressUpdater.run();
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
updateButton2.setVisibility(View.GONE);
updateButton1.setEnabled(true);
updateButton1.setText(R.string.install_update);
}
}
}
private void onUpdateButtonClick(View v){
if(v.getId()==R.id.button){
GithubSelfUpdater.UpdateState state=GithubSelfUpdater.getInstance().getState();
if(state==GithubSelfUpdater.UpdateState.UPDATE_AVAILABLE){
GithubSelfUpdater.getInstance().downloadUpdate();
}else if(state==GithubSelfUpdater.UpdateState.DOWNLOADED){
GithubSelfUpdater.getInstance().installUpdate(getActivity());
}
}else if(v.getId()==R.id.button2){
GithubSelfUpdater.getInstance().cancelDownload();
}
}
}

View File

@@ -0,0 +1,315 @@
package org.joinmastodon.android.fragments.settings;
import android.app.AlertDialog;
import android.app.NotificationManager;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.PushSubscriptionManager;
import org.joinmastodon.android.api.session.AccountLocalPreferences;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.PushSubscription;
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
import org.joinmastodon.android.ui.utils.UiUtils;
import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.utils.MergeRecyclerAdapter;
import me.grishka.appkit.utils.V;
public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
private PushSubscription pushSubscription;
private CheckableListItem<Void> pauseItem;
private ListItem<Void> policyItem;
private MergeRecyclerAdapter mergeAdapter;
private HideableSingleViewRecyclerAdapter bannerAdapter;
private ImageView bannerIcon;
private TextView bannerText;
private Button bannerButton;
private CheckableListItem<Void> mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem;
private List<CheckableListItem<Void>> typeItems;
private boolean needUpdateNotificationSettings;
private boolean notificationsAllowed=true;
// MEGALODON
private CheckableListItem<Void> uniformIconItem, deleteItem, onlyLatestItem;
private CheckableListItem<Void> postsItem, updateItem;
private AccountLocalPreferences lp;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setTitle(R.string.settings_notifications);
lp=AccountSessionManager.get(accountID).getLocalPreferences();
getPushSubscription();
onDataLoaded(List.of(
pauseItem=new CheckableListItem<>(getString(R.string.pause_all_notifications), getPauseItemSubtitle(), CheckableListItem.Style.SWITCH, false, R.drawable.ic_fluent_alert_snooze_24_regular, ()->onPauseNotificationsClick(false)),
policyItem=new ListItem<>(R.string.settings_notifications_policy, 0, R.drawable.ic_fluent_people_24_regular, this::onNotificationsPolicyClick, 0, true),
mentionsItem=new CheckableListItem<>(R.string.notification_type_mentions_and_replies, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.mention, R.drawable.ic_fluent_mention_24_regular, ()->toggleCheckableItem(mentionsItem)),
boostsItem=new CheckableListItem<>(R.string.notification_type_reblog, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.reblog, R.drawable.ic_fluent_arrow_repeat_all_24_regular, ()->toggleCheckableItem(boostsItem)),
favoritesItem=new CheckableListItem<>(R.string.notification_type_favorite, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.favourite, R.drawable.ic_fluent_star_24_regular, ()->toggleCheckableItem(favoritesItem)),
followersItem=new CheckableListItem<>(R.string.notification_type_follow, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.follow, R.drawable.ic_fluent_person_add_24_regular, ()->toggleCheckableItem(followersItem)),
pollsItem=new CheckableListItem<>(R.string.notification_type_poll, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.poll, R.drawable.ic_fluent_poll_24_regular, ()->toggleCheckableItem(pollsItem)),
updateItem=new CheckableListItem<>(R.string.sk_notification_type_update, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.update, R.drawable.ic_fluent_history_24_regular, ()->toggleCheckableItem(updateItem)),
postsItem=new CheckableListItem<>(R.string.sk_notification_type_posts, 0, CheckableListItem.Style.CHECKBOX, pushSubscription.alerts.status, R.drawable.ic_fluent_chat_24_regular, ()->toggleCheckableItem(postsItem), true),
uniformIconItem=new CheckableListItem<>(R.string.sk_settings_uniform_icon_for_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.uniformNotificationIcon, R.drawable.ic_ntf_logo, ()->toggleCheckableItem(uniformIconItem)),
deleteItem=new CheckableListItem<>(R.string.sk_settings_enable_delete_notifications, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enableDeleteNotifications, R.drawable.ic_fluent_mail_inbox_dismiss_24_regular, ()->toggleCheckableItem(deleteItem)),
onlyLatestItem=new CheckableListItem<>(R.string.sk_settings_single_notification, 0, CheckableListItem.Style.SWITCH, lp.keepOnlyLatestNotification, R.drawable.ic_fluent_convert_range_24_regular, ()->toggleCheckableItem(onlyLatestItem), true)
));
typeItems=List.of(mentionsItem, boostsItem, favoritesItem, followersItem, pollsItem, updateItem, postsItem);
pauseItem.checkedChangeListener=checked->onPauseNotificationsClick(true);
updatePolicyItem(null);
updatePauseItem();
}
@Override
protected void doLoadData(int offset, int count){}
@Override
protected void onHidden(){
super.onHidden();
PushSubscription ps=getPushSubscription();
needUpdateNotificationSettings|=mentionsItem.checked!=ps.alerts.mention
|| boostsItem.checked!=ps.alerts.reblog
|| favoritesItem.checked!=ps.alerts.favourite
|| followersItem.checked!=ps.alerts.follow
|| pollsItem.checked!=ps.alerts.poll;
GlobalUserPreferences.uniformNotificationIcon=uniformIconItem.checked;
GlobalUserPreferences.enableDeleteNotifications=deleteItem.checked;
GlobalUserPreferences.save();
lp.keepOnlyLatestNotification=onlyLatestItem.checked;
lp.save();
if(needUpdateNotificationSettings && PushSubscriptionManager.arePushNotificationsAvailable()){
ps.alerts.mention=mentionsItem.checked;
ps.alerts.reblog=boostsItem.checked;
ps.alerts.favourite=favoritesItem.checked;
ps.alerts.follow=followersItem.checked;
ps.alerts.poll=pollsItem.checked;
ps.alerts.status=postsItem.checked;
ps.alerts.update=updateItem.checked;
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().updatePushSettings(pushSubscription);
}
}
@Override
protected void onShown(){
super.onShown();
boolean allowed=areNotificationsAllowed();
PushSubscription ps=getPushSubscription();
if(allowed!=notificationsAllowed){
notificationsAllowed=allowed;
updateBanner();
pauseItem.isEnabled=allowed;
policyItem.isEnabled=allowed;
rebindItem(pauseItem);
rebindItem(policyItem);
for(CheckableListItem<Void> item:typeItems){
item.isEnabled=allowed && ps.policy!=PushSubscription.Policy.NONE;
rebindItem(item);
}
}
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
View banner=getActivity().getLayoutInflater().inflate(R.layout.item_settings_banner, list, false);
bannerText=banner.findViewById(R.id.text);
bannerIcon=banner.findViewById(R.id.icon);
bannerButton=banner.findViewById(R.id.button);
bannerAdapter=new HideableSingleViewRecyclerAdapter(banner);
bannerAdapter.setVisible(false);
banner.findViewById(R.id.button2).setVisibility(View.GONE);
banner.findViewById(R.id.title).setVisibility(View.GONE);
((RelativeLayout.LayoutParams) bannerText.getLayoutParams())
.setMargins(0, V.dp(4), 0, 0);
((RelativeLayout.LayoutParams) bannerIcon.getLayoutParams())
.addRule(RelativeLayout.CENTER_VERTICAL);
RelativeLayout.LayoutParams buttonParams = (RelativeLayout.LayoutParams) bannerButton.getLayoutParams();
buttonParams.setMargins(buttonParams.leftMargin, V.dp(-8), buttonParams.rightMargin, V.dp(-12));
mergeAdapter=new MergeRecyclerAdapter();
mergeAdapter.addAdapter(bannerAdapter);
mergeAdapter.addAdapter(super.getAdapter());
return mergeAdapter;
}
@Override
protected int indexOfItemsAdapter(){
return mergeAdapter.getPositionForAdapter(itemsAdapter);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
updateBanner();
}
private boolean areNotificationsAllowed(){
return Build.VERSION.SDK_INT<Build.VERSION_CODES.N || getActivity().getSystemService(NotificationManager.class).areNotificationsEnabled();
}
private PushSubscription getPushSubscription(){
if(pushSubscription!=null)
return pushSubscription;
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
if(session.pushSubscription==null){
pushSubscription=new PushSubscription();
pushSubscription.alerts=PushSubscription.Alerts.ofAll();
}else{
pushSubscription=session.pushSubscription.clone();
}
return pushSubscription;
}
private String getPauseItemSubtitle(){
return getString(R.string.pause_notifications_off);
}
private void resumePausedNotifications(){
AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(0);
updatePauseItem();
}
private void openSystemNotificationSettings(){
Intent intent;
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
intent=new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, Uri.fromParts("package", getActivity().getPackageName(), null));
}else{
intent=new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getActivity().getPackageName());
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
private void onPauseNotificationsClick(boolean fromSwitch){
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
if(time>System.currentTimeMillis() && fromSwitch){
resumePausedNotifications();
return;
}
int[] durationOptions={
1800,
3600,
12*3600,
24*3600,
3*24*3600,
7*24*3600
};
int[] selectedOption={0};
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.pause_all_notifications_title)
.setSupportingText(time>System.currentTimeMillis() ? getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false)) : null)
.setSingleChoiceItems((String[])Arrays.stream(durationOptions).mapToObj(d->UiUtils.formatDuration(getActivity(), d)).toArray(String[]::new), -1, (dlg, item)->{
if(selectedOption[0]==0){
((AlertDialog)dlg).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true);
}
selectedOption[0]=durationOptions[item];
})
.setPositiveButton(R.string.ok, (dlg, item)->AccountSessionManager.get(accountID).getLocalPreferences().setNotificationsPauseEndTime(System.currentTimeMillis()+selectedOption[0]*1000L))
.setNegativeButton(R.string.cancel, null)
.show();
alert.setOnDismissListener(dialog->updatePauseItem());
alert.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
}
private void onNotificationsPolicyClick(){
String[] items=Stream.of(
R.string.notifications_policy_anyone,
R.string.notifications_policy_followed,
R.string.notifications_policy_follower,
R.string.notifications_policy_no_one
).map(this::getString).toArray(String[]::new);
int[] selectedItem={getPushSubscription().policy.ordinal()};
new M3AlertDialogBuilder(getActivity())
.setTitle(R.string.settings_notifications_policy)
.setSingleChoiceItems(items, selectedItem[0], (dlg, which)->selectedItem[0]=which)
.setPositiveButton(R.string.ok, (dlg, which)->{
PushSubscription.Policy prevValue=getPushSubscription().policy;
PushSubscription.Policy newValue=PushSubscription.Policy.values()[selectedItem[0]];
if(prevValue==newValue)
return;
getPushSubscription().policy=newValue;
updatePolicyItem(prevValue);
needUpdateNotificationSettings=true;
})
.setNegativeButton(R.string.cancel, null)
.show();
}
private void updatePolicyItem(PushSubscription.Policy prevValue){
policyItem.subtitleRes=switch(getPushSubscription().policy){
case ALL -> R.string.notifications_policy_anyone;
case FOLLOWED -> R.string.notifications_policy_followed;
case FOLLOWER -> R.string.notifications_policy_follower;
case NONE -> R.string.notifications_policy_no_one;
};
rebindItem(policyItem);
if(pushSubscription.policy==PushSubscription.Policy.NONE || prevValue==PushSubscription.Policy.NONE){
for(CheckableListItem<Void> item:typeItems){
item.checked=item.isEnabled=prevValue==PushSubscription.Policy.NONE;
rebindItem(item);
}
}
}
private void updatePauseItem(){
long time=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
if(time<System.currentTimeMillis()){
pauseItem.subtitle=getString(R.string.pause_notifications_off);
pauseItem.checked=false;
}else{
pauseItem.subtitle=getString(R.string.pause_notifications_ends, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(time), false));
pauseItem.checked=true;
}
rebindItem(pauseItem);
updateBanner();
}
private void updateBanner(){
if(bannerAdapter==null)
return;
long pauseTime=AccountSessionManager.get(accountID).getLocalPreferences().getNotificationsPauseEndTime();
if(!areNotificationsAllowed()){
bannerAdapter.setVisible(true);
bannerIcon.setImageResource(R.drawable.ic_fluent_alert_badge_24_regular);
bannerText.setText(R.string.notifications_disabled_in_system);
bannerButton.setText(R.string.open_system_notification_settings);
bannerButton.setOnClickListener(v->openSystemNotificationSettings());
}else if(pauseTime>System.currentTimeMillis()){
bannerAdapter.setVisible(true);
bannerIcon.setImageResource(R.drawable.ic_fluent_alert_snooze_24_regular);
bannerText.setText(getString(R.string.pause_notifications_banner, UiUtils.formatRelativeTimestampAsMinutesAgo(getActivity(), Instant.ofEpochMilli(pauseTime), false)));
bannerButton.setText(R.string.resume_notifications_now);
bannerButton.setOnClickListener(v->resumePausedNotifications());
}else{
bannerAdapter.setVisible(false);
}
}
}

View File

@@ -0,0 +1,247 @@
package org.joinmastodon.android.fragments.settings;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.api.requests.instance.GetInstanceExtendedDescription;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
import org.joinmastodon.android.model.viewmodel.ListItem;
import org.joinmastodon.android.ui.OutlineProviders;
import org.joinmastodon.android.ui.utils.UiUtils;
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
import org.joinmastodon.android.ui.views.FixedAspectRatioImageView;
import org.joinmastodon.android.utils.ViewImageLoaderHolderTarget;
import org.parceler.Parcels;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
import me.grishka.appkit.fragments.LoaderFragment;
import me.grishka.appkit.imageloader.ViewImageLoader;
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
import me.grishka.appkit.utils.V;
public class SettingsServerAboutFragment extends LoaderFragment{
private String accountID;
private Instance instance;
private WebView webView;
private LinearLayout scrollingLayout;
public ScrollView scroller;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
loadData();
}
@SuppressLint("SetJavaScriptEnabled")
@Override
public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
webView=new WebView(getActivity());
webView.getSettings().setJavaScriptEnabled(true);
webView.setWebViewClient(new WebViewClient(){
@Override
public void onPageFinished(WebView view, String url){
dataLoaded();
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url){
Uri uri=Uri.parse(url);
if(uri.getScheme().equals("http") || uri.getScheme().equals("https")){
UiUtils.launchWebBrowser(getActivity(), url);
}else{
Intent intent=new Intent(Intent.ACTION_VIEW, uri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try{
startActivity(intent);
}catch(ActivityNotFoundException x){
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
}
}
return true;
}
});
scrollingLayout=new LinearLayout(getActivity());
scrollingLayout.setOrientation(LinearLayout.VERTICAL);
scroller=new ScrollView(getActivity());
scroller.setNestedScrollingEnabled(true);
scroller.setClipToPadding(false);
scroller.addView(scrollingLayout);
FixedAspectRatioImageView banner=new FixedAspectRatioImageView(getActivity());
banner.setAspectRatio(1.914893617f);
banner.setScaleType(ImageView.ScaleType.CENTER_CROP);
banner.setOutlineProvider(OutlineProviders.bottomRoundedRect(16));
banner.setClipToOutline(true);
ViewImageLoader.loadWithoutAnimation(banner, getResources().getDrawable(R.drawable.image_placeholder, getActivity().getTheme()), new UrlImageLoaderRequest(instance.thumbnail));
LinearLayout.LayoutParams blp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
blp.bottomMargin=V.dp(24);
scrollingLayout.addView(banner, blp);
boolean needDivider=false;
if(instance.contactAccount!=null){
needDivider=true;
TextView heading=new TextView(getActivity());
heading.setTextAppearance(R.style.m3_title_small);
heading.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant));
heading.setSingleLine();
heading.setText(R.string.server_administrator);
heading.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams hlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(20));
hlp.bottomMargin=V.dp(4);
hlp.leftMargin=hlp.rightMargin=V.dp(16);
scrollingLayout.addView(heading, hlp);
AccountViewModel model=new AccountViewModel(instance.contactAccount, accountID);
AccountViewHolder holder=new AccountViewHolder(this, scrollingLayout, null);
holder.setStyle(AccountViewHolder.AccessoryType.NONE, false);
holder.bind(model);
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
holder.itemView.setOnClickListener(v->holder.onClick());
scrollingLayout.addView(holder.itemView);
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, 0), null, model.avaRequest, false);
for(int i=0;i<model.emojiHelper.getImageCount();i++){
ViewImageLoader.load(new ViewImageLoaderHolderTarget(holder, i+1), null, model.emojiHelper.getImageRequest(i), false);
}
}
if(!TextUtils.isEmpty(instance.email)){
needDivider=true;
SimpleListItemViewHolder holder=new SimpleListItemViewHolder(getActivity(), scrollingLayout);
ListItem<Void> item=new ListItem<>(R.string.send_email_to_server_admin, 0, R.drawable.ic_fluent_mail_24_regular, ()->{});
holder.bind(item);
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
holder.itemView.setOnClickListener(v->openAdminEmail());
scrollingLayout.addView(holder.itemView);
}
if(needDivider){
View divider=new View(getActivity());
divider.setBackgroundColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OutlineVariant));
LinearLayout.LayoutParams dlp=new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(1));
dlp.leftMargin=dlp.rightMargin=V.dp(16);
scrollingLayout.addView(divider, dlp);
}
scrollingLayout.addView(webView, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
return scroller;
}
@Override
protected void doLoadData(){
new GetInstanceExtendedDescription()
.setCallback(new Callback<>(){
@Override
public void onSuccess(GetInstanceExtendedDescription.Response result){
MastodonAPIController.runInBackground(()->{
Activity activity=getActivity();
if(activity==null)
return;
String template;
try(BufferedReader reader=new BufferedReader(new InputStreamReader(getActivity().getAssets().open("server_about_template.htm")))){
StringBuilder sb=new StringBuilder();
String line;
while((line=reader.readLine())!=null){
sb.append(line);
sb.append('\n');
}
template=sb.toString();
}catch(IOException x){
throw new RuntimeException(x);
}
HashMap<String, String> templateParams=new HashMap<>();
templateParams.put("content", result.content);
templateParams.put("colorSurface", getThemeColorAsCss(R.attr.colorM3Surface, 1));
templateParams.put("colorOnSurface", getThemeColorAsCss(R.attr.colorM3OnSurface, 1));
templateParams.put("colorPrimary", getThemeColorAsCss(R.attr.colorM3Primary, 1));
templateParams.put("colorPrimaryTransparent", getThemeColorAsCss(R.attr.colorM3Primary, 0.2f));
for(Map.Entry<String, String> param:templateParams.entrySet()){
template=template.replace("{{"+param.getKey()+"}}", param.getValue());
}
final String html=template;
activity.runOnUiThread(()->{
webView.loadDataWithBaseURL(null, html, "text/html; charset=utf-8", null, null);
});
});
}
@Override
public void onError(ErrorResponse error){
// probably an akkoma instance where this isn't implemented
dataLoaded();
}
})
.execRemote(instance.normalizedUri);
}
@Override
public void onRefresh(){}
private void openAdminEmail(){
Intent intent=new Intent(Intent.ACTION_VIEW, Uri.fromParts("mailto", instance.email, null));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
try{
startActivity(intent);
}catch(ActivityNotFoundException x){
Toast.makeText(getActivity(), R.string.no_app_to_handle_action, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
scroller.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
progress.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom());
insets=insets.inset(0, 0, 0, insets.getSystemWindowInsetBottom());
}else{
scroller.setPadding(0, 0, 0, 0);
}
super.onApplyWindowInsets(insets);
}
private String getThemeColorAsCss(int attr, float alpha){
int color=UiUtils.getThemeColor(getActivity(), attr);
if(alpha==1f){
return String.format(Locale.US, "#%06X", color & 0xFFFFFF);
}else{
int r=(color >> 16) & 0xFF;
int g=(color >> 8) & 0xFF;
int b=color & 0xFF;
return "rgba("+r+","+g+","+b+","+alpha+")";
}
}
}

View File

@@ -0,0 +1,192 @@
package org.joinmastodon.android.fragments.settings;
import android.app.Fragment;
import android.os.Build;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Instance;
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 org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
import org.parceler.Parcels;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager2.widget.ViewPager2;
import me.grishka.appkit.fragments.AppKitFragment;
import me.grishka.appkit.utils.V;
public class SettingsServerFragment extends AppKitFragment{
private String accountID;
private Instance instance;
private TabLayout tabBar;
private TabLayoutMediator tabLayoutMediator;
private ViewPager2 pager;
private FrameLayout[] tabViews;
private View contentView;
private WindowInsets childInsets;
private SettingsServerAboutFragment aboutFragment;
private SettingsServerRulesFragment rulesFragment;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
instance=getArguments().containsKey("instance")
? Parcels.unwrap(getArguments().getParcelable("instance"))
: AccountSessionManager.getOptional(accountID)
.map(i->AccountSessionManager.getInstance().getInstanceInfo(i.domain))
.orElseThrow();
setTitle(instance.title);
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
args.putParcelable("instance", Parcels.wrap(instance));
aboutFragment=new SettingsServerAboutFragment();
aboutFragment.setArguments(args);
rulesFragment=new SettingsServerRulesFragment();
rulesFragment.setArguments(args);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState){
View view=inflater.inflate(R.layout.fragment_settings_server, container, false);
TextView realTitle=view.findViewById(R.id.real_title);
realTitle.setText(getTitle());
realTitle.setSelected(true);
pager=view.findViewById(R.id.pager);
pager.setAdapter(new ServerPagerAdapter());
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
pager.getLayoutParams().height=MeasureSpec.getSize(heightMeasureSpec)-getPaddingTop()-getPaddingBottom()-V.dp(48);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
};
tabViews=new FrameLayout[2];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.server_about;
case 1 -> R.id.server_rules;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
sizeWrapper.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment
tabViews[i]=tabView;
}
tabBar=view.findViewById(R.id.tabbar);
tabBar.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant), UiUtils.getThemeColor(getActivity(), R.attr.colorM3Primary));
tabBar.setTabTextSize(V.dp(14));
tabLayoutMediator=new TabLayoutMediator(tabBar, pager, (tab, position)->tab.setText(switch(position){
case 0 -> R.string.about_server;
case 1 -> R.string.server_rules;
default -> throw new IllegalStateException("Unexpected value: "+position);
}));
tabLayoutMediator.attach();
NestedRecyclerScrollView scrollView=view.findViewById(R.id.scroller);
scrollView.setScrollableChildSupplier(()->switch(pager.getCurrentItem()){
case 0 -> aboutFragment.scroller;
case 1 -> rulesFragment.getList();
default -> throw new IllegalStateException("Unexpected value: "+pager.getCurrentItem());
});
return contentView=view;
}
@Override
protected void onUpdateToolbar(){
super.onUpdateToolbar();
getToolbar().setTitle(null);
}
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> aboutFragment;
case 1 -> rulesFragment;
default -> throw new IllegalStateException();
};
}
@Override
public void onApplyWindowInsets(WindowInsets insets){
if(contentView!=null){
if(Build.VERSION.SDK_INT>=29 && insets.getTappableElementInsets().bottom==0){
int insetBottom=insets.getSystemWindowInsetBottom();
childInsets=insets.inset(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0);
applyChildWindowInsets();
insets=insets.inset(0, 0, 0, insetBottom);
}
}
super.onApplyWindowInsets(insets);
}
private void applyChildWindowInsets(){
if(aboutFragment!=null && aboutFragment.isAdded() && childInsets!=null){
aboutFragment.onApplyWindowInsets(childInsets);
rulesFragment.onApplyWindowInsets(childInsets);
}
}
private class ServerPagerAdapter extends RecyclerView.Adapter<SimpleViewHolder>{
@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){
Fragment fragment=getFragmentForPage(position);
if(!fragment.isAdded()){
getChildFragmentManager().beginTransaction().add(holder.itemView.getId(), fragment).commit();
holder.itemView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){
@Override
public boolean onPreDraw(){
getChildFragmentManager().executePendingTransactions();
if(fragment.isAdded()){
holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
applyChildWindowInsets();
}
return true;
}
});
}
}
@Override
public int getItemCount(){
return 2;
}
@Override
public int getItemViewType(int position){
return position;
}
}
}

View File

@@ -0,0 +1,56 @@
package org.joinmastodon.android.fragments.settings;
import android.os.Bundle;
import org.joinmastodon.android.api.requests.instance.GetInstance;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.ui.adapters.InstanceRulesAdapter;
import org.parceler.Parcels;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.api.Callback;
import me.grishka.appkit.api.ErrorResponse;
public class SettingsServerRulesFragment extends MastodonRecyclerFragment<Instance.Rule>{
private String accountID;
private String domain;
public SettingsServerRulesFragment(){
super(20);
}
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
domain=getArguments().getString("domain");
Instance instance=Parcels.unwrap(getArguments().getParcelable("instance"));
onDataLoaded(instance.rules);
setRefreshEnabled(false);
}
@Override
protected void doLoadData(int offset, int count){
new GetInstance().setCallback(new Callback<>(){
@Override
public void onSuccess(Instance instance){
onDataLoaded(instance.rules);
}
@Override
public void onError(ErrorResponse error){
error.showToast(getContext());
}
}).execRemote(domain);
}
@Override
protected RecyclerView.Adapter<?> getAdapter(){
return new InstanceRulesAdapter(data);
}
public RecyclerView getList(){
return list;
}
}