From 123f5aa22e5450607bb1476781e1b294e31e64ff Mon Sep 17 00:00:00 2001 From: LucasGGamerM Date: Sun, 18 May 2025 09:23:12 -0300 Subject: [PATCH] refactor(HomeTabFragment.java): bring back the working HomeTabFragment implementation --- .../android/fragments/HomeTabFragment.java | 1546 +++++++---------- 1 file changed, 669 insertions(+), 877 deletions(-) diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java index 3d14eeb80..b5d60517f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java @@ -1,361 +1,529 @@ package org.joinmastodon.android.fragments; +import static org.joinmastodon.android.GlobalUserPreferences.reduceMotion; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.Fragment; +import android.app.FragmentTransaction; +import android.app.assist.AssistContent; import android.content.Context; -import android.content.Intent; import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; -import android.os.VibrationEffect; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.text.style.ForegroundColorSpan; -import android.text.style.TypefaceSpan; -import android.text.style.UnderlineSpan; -import android.view.Gravity; +import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; -import android.view.ViewStub; +import android.view.ViewParent; import android.view.ViewTreeObserver; -import android.view.accessibility.AccessibilityNodeInfo; -import android.view.animation.AnimationUtils; import android.widget.Button; import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; -import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.TextView; -import android.widget.Toast; import android.widget.Toolbar; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager2.widget.ViewPager2; + import com.squareup.otto.Subscribe; -import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.E; +import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.MastodonAPIRequest; -import org.joinmastodon.android.api.requests.catalog.GetDonationCampaigns; -import org.joinmastodon.android.api.requests.markers.SaveMarkers; -import org.joinmastodon.android.api.requests.timelines.GetHomeTimeline; -import org.joinmastodon.android.api.requests.timelines.GetListTimeline; -import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; +import org.joinmastodon.android.api.requests.announcements.GetAnnouncements; +import org.joinmastodon.android.api.requests.lists.GetLists; +import org.joinmastodon.android.api.requests.tags.GetFollowedTags; import org.joinmastodon.android.api.session.AccountSessionManager; -import org.joinmastodon.android.events.DismissDonationCampaignBannerEvent; +import org.joinmastodon.android.events.HashtagUpdatedEvent; +import org.joinmastodon.android.events.ListCreatedEvent; +import org.joinmastodon.android.events.ListDeletedEvent; +import org.joinmastodon.android.events.ListUpdatedEvent; import org.joinmastodon.android.events.SelfUpdateStateChangedEvent; import org.joinmastodon.android.fragments.settings.SettingsMainFragment; -import org.joinmastodon.android.model.CacheablePaginatedResponse; -import org.joinmastodon.android.model.FilterContext; +import org.joinmastodon.android.model.Announcement; +import org.joinmastodon.android.model.Hashtag; +import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.FollowList; -import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.model.TimelineMarkers; -import org.joinmastodon.android.model.donations.DonationCampaign; -import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; -import org.joinmastodon.android.ui.sheets.DonationSheet; -import org.joinmastodon.android.ui.sheets.DonationSuccessfulSheet; -import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; +import org.joinmastodon.android.model.StatusPrivacy; +import org.joinmastodon.android.model.TimelineDefinition; +import org.joinmastodon.android.model.viewmodel.ListItem; +import org.joinmastodon.android.ui.ExtendedPopupMenu; +import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.utils.UiUtils; -import org.joinmastodon.android.ui.viewcontrollers.HomeTimelineMenuController; -import org.joinmastodon.android.ui.viewcontrollers.ToolbarDropdownMenuController; -import org.joinmastodon.android.ui.views.FixedAspectRatioImageView; -import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; -import org.joinmastodon.android.ui.views.NewPostsButtonContainer; import org.joinmastodon.android.updater.GithubSelfUpdater; -import org.parceler.Parcels; +import org.joinmastodon.android.utils.ElevationOnScrollListener; +import org.joinmastodon.android.utils.ProvidesAssistContent; import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.LinearLayoutManager; -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.fragments.BaseRecyclerFragment; import me.grishka.appkit.utils.CubicBezierInterpolator; -import me.grishka.appkit.utils.MergeRecyclerAdapter; import me.grishka.appkit.utils.V; -import me.grishka.appkit.views.BottomSheet; +import me.grishka.appkit.views.FragmentRootLinearLayout; -public class HomeTabFragment extends StatusListFragment implements ToolbarDropdownMenuController.HostFragment{ - private static final int DONATION_RESULT=211; +public class HomeTabFragment extends MastodonToolbarFragment implements ScrollableToTop, HasFab, ProvidesAssistContent, HasElevationOnScrollListener { + private static final int ANNOUNCEMENTS_RESULT = 654; - private ImageButton fab; - private LinearLayout listsDropdown; - private FixedAspectRatioImageView listsDropdownArrow; - private TextView listsDropdownText; - private Button newPostsBtn; - private NewPostsButtonContainer newPostsBtnWrap; + private String accountID; + private MenuItem announcements, announcementsAction, settings, settingsAction; + // private ImageView toolbarLogo; + private Button toolbarShowNewPostsBtn; private boolean newPostsBtnShown; private AnimatorSet currentNewPostsAnim; - private ToolbarDropdownMenuController dropdownController; - private HomeTimelineMenuController dropdownMainMenuController; - private List lists=List.of(); - private ListMode listMode=ListMode.FOLLOWING; - private FollowList currentList; - private MergeRecyclerAdapter mergeAdapter; - private DiscoverInfoBannerHelper localTimelineBannerHelper; - private View donationBanner; - private boolean donationBannerDismissing; - private NestedRecyclerScrollView scrollWrapper; + private ViewPager2 pager; + private View switcher; + private FrameLayout toolbarFrame; + private ImageView timelineIcon; + private ImageView collapsedChevron; + private TextView timelineTitle; + private PopupMenu switcherPopup; + private final Map listItems = new HashMap<>(); + private final Map hashtagsItems = new HashMap<>(); + private List timelinesList; + private int count; + private Fragment[] fragments; + private FrameLayout[] tabViews; + private TimelineDefinition[] timelines; + private final Map timelinesByMenuItem = new HashMap<>(); + private SubMenu hashtagsMenu, listsMenu; + private PopupMenu overflowPopup; + private View overflowActionView = null; + private boolean announcementsBadged, settingsBadged; + private ImageButton fab; + private ElevationOnScrollListener elevationOnScrollListener; - private String scrollBackItemID; - private int scrollBackItemOffset, scrollBackItemIndex; - private long scrollBackTime; - - private String maxID; - private String lastSavedMarkerID; - private DonationCampaign currentDonationCampaign; - private BottomSheet donationSheet; - - public HomeTabFragment(){ - setLayout(R.layout.fragment_loader_hiding_toolbar); - setListLayoutId(R.layout.fragment_timeline); - } + // TODO: rename this + private Runnable returnToBeginningOfPager=this::returnToBeginningOfPager; @Override - public void onCreate(Bundle savedInstanceState){ + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - localTimelineBannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.LOCAL_TIMELINE, accountID); - - if(AccountSessionManager.get(accountID).isEligibleForDonations()){ - GetDonationCampaigns req=new GetDonationCampaigns(Locale.getDefault().toLanguageTag().replace('-', '_'), String.valueOf(AccountSessionManager.get(accountID).getDonationSeed()), null); - if(getActivity().getSharedPreferences("debug", Context.MODE_PRIVATE).getBoolean("donationsStaging", false)){ - req.setStaging(true); - } - req.setCallback(new Callback<>(){ - @Override - public void onSuccess(DonationCampaign result){ - if(result==null) - return; - AccountSessionManager.getInstance().runIfDonationCampaignNotDismissed(result.id, ()->showDonationBanner(result)); - } - - @Override - public void onError(ErrorResponse error){} - }) - .execNoAuth(""); - } E.register(this); - } - - @Override - public void onDestroy(){ - super.onDestroy(); - E.unregister(this); - } - - @Override - public void onAttach(Activity activity){ - super.onAttach(activity); - dropdownController=new ToolbarDropdownMenuController(this); - dropdownMainMenuController=new HomeTimelineMenuController(dropdownController, new HomeTimelineMenuController.Callback(){ - @Override - public void onFollowingSelected(){ - if(listMode==ListMode.FOLLOWING) - return; - listMode=ListMode.FOLLOWING; - reload(); - } - - @Override - public void onLocalSelected(){ - if(listMode==ListMode.LOCAL) - return; - listMode=ListMode.LOCAL; - reload(); - } - - @Override - public List getLists(){ - return lists; - } - - @Override - public void onListSelected(FollowList list){ - if(listMode==ListMode.LIST && currentList==list) - return; - listMode=ListMode.LIST; - currentList=list; - reload(); - } - }); - setHasOptionsMenu(true); - loadData(); - AccountSessionManager.get(accountID).getCacheController().getLists(new Callback<>(){ - @Override - public void onSuccess(List result){ - lists=result; - } - - @Override - public void onError(ErrorResponse error){} - }); - } - - @Override - protected void doLoadData(int offset, int count){ - switch(listMode){ - case FOLLOWING -> { - AccountSessionManager.getInstance() - .getAccount(accountID).getCacheController() - .getHomeTimeline(offset>0 ? maxID : null, count, refreshing, new SimpleCallback<>(this){ - @Override - public void onSuccess(CacheablePaginatedResponse> result){ - if(getActivity()==null || listMode!=ListMode.FOLLOWING) - return; - if(refreshing) - list.scrollToPosition(0); - onDataLoaded(result.items, !result.items.isEmpty()); - maxID=result.maxID; - if(result.isFromCache()) - loadNewPosts(); - } - - @Override - public void onError(ErrorResponse error){ - if(listMode!=ListMode.FOLLOWING) - return; - super.onError(error); - } - }); - } - case LOCAL -> { - currentRequest=new GetPublicTimeline(true, false, offset>0 ? maxID : null, null, count, null) - .setCallback(new SimpleCallback<>(this){ - @Override - public void onSuccess(List result){ - if(refreshing) - list.scrollToPosition(0); - maxID=result.isEmpty() ? null : result.get(result.size()-1).id; - AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.PUBLIC); - onDataLoaded(result, !result.isEmpty()); - } - }) - .exec(accountID); - } - case LIST -> { - currentRequest=new GetListTimeline(currentList.id, offset>0 ? maxID : null, null, count, null) - .setCallback(new SimpleCallback<>(this){ - @Override - public void onSuccess(List result){ - if(refreshing) - list.scrollToPosition(0); - maxID=result.isEmpty() ? null : result.get(result.size()-1).id; - AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME); - onDataLoaded(result, !result.isEmpty()); - } - }) - .exec(accountID); - } + accountID = getArguments().getString("account"); + 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]; + if(GlobalUserPreferences.toolbarMarquee){ + setTitleMarqueeEnabled(false); + setSubtitleMarqueeEnabled(false); } } + private void returnToBeginningOfPager() { + pager.setCurrentItem(0); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + setHasOptionsMenu(true); + } + + @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); + fab.setOnLongClickListener(this::onFabLongClick); + pager = new ViewPager2(getContext()); + toolbarFrame = (FrameLayout) LayoutInflater.from(getContext()).inflate(R.layout.home_toolbar, getToolbar(), false); + + if (fragments[0] == null) { + Bundle args = new Bundle(); + args.putString("account", accountID); + args.putBoolean("__is_tab", true); + args.putBoolean("__disable_fab", true); + args.putBoolean("onlyPosts", true); + + for (int i=0; i < timelinesList.size(); i++) { + TimelineDefinition tl = timelinesList.get(i); + fragments[i] = tl.getFragment(); + timelines[i] = tl; + } + + FragmentTransaction transaction = getChildFragmentManager().beginTransaction(); + for (int i = 0; i < count; i++) { + fragments[i].setArguments(timelines[i].populateArguments(new Bundle(args))); + FrameLayout tabView = new FrameLayout(getActivity()); + tabView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + tabView.setVisibility(View.GONE); + tabView.setId(i + 1); + transaction.add(i + 1, fragments[i]); + view.addView(tabView); + tabViews[i] = tabView; + } + transaction.commit(); + } + + view.addView(pager, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + overflowActionView = UiUtils.makeOverflowActionView(getContext()); + overflowPopup = new PopupMenu(getContext(), overflowActionView); + overflowPopup.setOnMenuItemClickListener(this::onOptionsItemSelected); + overflowActionView.setOnClickListener(l -> overflowPopup.show()); + overflowActionView.setOnTouchListener(overflowPopup.getDragToOpenListener()); + + return rootView; + } + @SuppressLint("ClickableViewAccessibility") @Override public void onViewCreated(View view, Bundle savedInstanceState){ super.onViewCreated(view, savedInstanceState); - fab=view.findViewById(R.id.fab); - fab.setOnClickListener(this::onFabClick); - newPostsBtn=view.findViewById(R.id.new_posts_btn); - newPostsBtn.setOnClickListener(this::onNewPostsBtnClick); - newPostsBtnWrap=view.findViewById(R.id.new_posts_btn_wrap); - if(newPostsBtnShown){ - newPostsBtnWrap.setVisibility(View.VISIBLE); - }else{ - newPostsBtnWrap.setVisibility(View.GONE); - newPostsBtnWrap.setScaleX(0.9f); - newPostsBtnWrap.setScaleY(0.9f); - newPostsBtnWrap.setAlpha(0f); - newPostsBtnWrap.setTranslationY(V.dp(-56)); + timelineIcon = toolbarFrame.findViewById(R.id.timeline_icon); + timelineTitle = toolbarFrame.findViewById(R.id.timeline_title); + collapsedChevron = toolbarFrame.findViewById(R.id.collapsed_chevron); + switcher = toolbarFrame.findViewById(R.id.switcher_btn); + switcherPopup = new PopupMenu(getContext(), switcher); + switcherPopup.setOnMenuItemClickListener(this::onSwitcherItemSelected); + UiUtils.enablePopupMenuIcons(getContext(), switcherPopup); + switcher.setOnClickListener(v->switcherPopup.show()); + switcher.setOnTouchListener(switcherPopup.getDragToOpenListener()); + updateSwitcherMenu(); + + UiUtils.reduceSwipeSensitivity(pager); + pager.setUserInputEnabled(!GlobalUserPreferences.disableSwipe); + pager.setAdapter(new HomePagerAdapter()); + pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position){ + if(position!=0) { + addBackCallback(returnToBeginningOfPager); + } else { + removeBackCallback(returnToBeginningOfPager); + } + + if (!reduceMotion) { + // setting this here because page transformer appears to fire too late so the + // animation can appear bumpy, especially when navigating to a further-away tab + switcher.setScaleY(0.85f); + switcher.setScaleX(0.85f); + switcher.setAlpha(0.65f); + } + updateSwitcherIcon(position); + if (!timelines[position].equals(TimelineDefinition.HOME_TIMELINE)) hideNewPostsButton(); + if (fragments[position] instanceof BaseRecyclerFragment page){ + if(!page.loaded && !page.isDataLoading()) page.loadData(); + } + } + }); + + if (!reduceMotion) { + pager.setPageTransformer((v, pos) -> { + if (reduceMotion || tabViews[pager.getCurrentItem()] != v) return; + float scaleFactor = Math.max(0.85f, 1 - Math.abs(pos) * 0.06f); + switcher.setScaleY(scaleFactor); + switcher.setScaleX(scaleFactor); + switcher.setAlpha(Math.max(0.65f, 1 - Math.abs(pos))); + }); } - newPostsBtnWrap.setOnHideButtonListener(this::hideNewPostsButton); - updateToolbarLogo(); - list.addOnScrollListener(new RecyclerView.OnScrollListener(){ - private HashSet gaps=new HashSet<>(); - @Override - public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){ - if(newPostsBtnShown && list.getChildAdapterPosition(list.getChildAt(0))<=getMainAdapterOffset()){ - hideNewPostsButton(); - } - for(StatusDisplayItem item:displayItems){ - if(item instanceof GapStatusDisplayItem gap){ - gaps.add(gap); - } - } - if(gaps.isEmpty()) - return; - for(int i=0;ilist); - scroller.setTakePriorityOverChildViews(true); - scroller.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY)->{ - bottomOverlays.setTranslationY(scrollY-getToolbar().getHeight()); - }); - scroller.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ - @Override - public boolean onPreDraw(){ - scroller.getViewTreeObserver().removeOnPreDrawListener(this); - bottomOverlays.setTranslationY(scroller.getScrollY()-getToolbar().getHeight()); - return true; - } - }); - scrollWrapper=scroller; + updateToolbarLogo(); + + ViewTreeObserver vto = getToolbar().getViewTreeObserver(); + if (vto.isAlive()) { + vto.addOnGlobalLayoutListener(()->{ + Toolbar t=getToolbar(); + if(t==null) return; + int toolbarWidth=t.getWidth(); + if(toolbarWidth==0) return; + + int toolbarFrameWidth=toolbarFrame.getWidth(); + int actionsWidth=toolbarWidth-toolbarFrameWidth; + // margin (4) + padding (12) + icon (24) + margin (8) + chevron (16) + padding (12) + int switcherWidth=V.dp(76); + FrameLayout parent=((FrameLayout) toolbarShowNewPostsBtn.getParent()); + if(actionsWidth==parent.getPaddingStart()) return; + int paddingMax=Math.max(actionsWidth, switcherWidth); + int paddingEnd=(Math.max(0, switcherWidth-actionsWidth)); + + // toolbar frame goes from screen edge to beginning of right-aligned option buttons. + // centering button by applying the same space on the left + parent.setPaddingRelative(paddingMax, 0, paddingEnd, 0); + toolbarShowNewPostsBtn.setMaxWidth(toolbarWidth-paddingMax*2); + + switcher.setPivotX(V.dp(28)); // padding + half of icon + switcher.setPivotY(switcher.getHeight() / 2f); + }); + } + + elevationOnScrollListener = new ElevationOnScrollListener((FragmentRootLinearLayout) view, getToolbar()); if(GithubSelfUpdater.needSelfUpdating()){ updateUpdateState(GithubSelfUpdater.getInstance().getState()); } - if(currentDonationCampaign!=null) - showDonationBanner(currentDonationCampaign); + + new GetLists().setCallback(new Callback<>() { + @Override + public void onSuccess(List lists) { + updateList(lists, listItems); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getContext()); + } + }).exec(accountID); + + new GetFollowedTags(null, 200).setCallback(new Callback<>() { + @Override + public void onSuccess(HeaderPaginationList hashtags) { + updateList(hashtags, hashtagsItems); + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getContext()); + } + }).exec(accountID); + + new GetAnnouncements(false).setCallback(new Callback<>() { + @Override + public void onSuccess(List result) { + if(getActivity()==null) return; + if (result.stream().anyMatch(a -> !a.read)) { + announcementsBadged = true; + announcements.setVisible(false); + announcementsAction.setVisible(true); + } + } + + @Override + public void onError(ErrorResponse error) { + error.showToast(getActivity()); + } + }).exec(accountID); + } + + public ElevationOnScrollListener getElevationOnScrollListener() { + return elevationOnScrollListener; + } + + private void onFabClick(View v){ + if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment l) { + l.onFabClick(v); + } + } + + private boolean onFabLongClick(View v) { + if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment l) { + return l.onFabLongClick(v); + } else { + return false; + } + } + + private void addListsToOverflowMenu() { + Context ctx = getContext(); + listsMenu.clear(); + listsMenu.getItem().setVisible(listItems.size() > 0); + UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(listsMenu)); + listItems.forEach((id, list) -> { + MenuItem item = listsMenu.add(Menu.NONE, id, Menu.NONE, list.title); + item.setIcon(R.drawable.ic_fluent_people_24_regular); + UiUtils.insetPopupMenuIcon(ctx, item); + }); + } + + private void addHashtagsToOverflowMenu() { + Context ctx = getContext(); + hashtagsMenu.clear(); + hashtagsMenu.getItem().setVisible(hashtagsItems.size() > 0); + UiUtils.insetPopupMenuIcon(ctx, UiUtils.makeBackItem(hashtagsMenu)); + hashtagsItems.entrySet().stream() + .sorted(Comparator.comparing(x -> x.getValue().name, String.CASE_INSENSITIVE_ORDER)) + .forEach(entry -> { + MenuItem item = hashtagsMenu.add(Menu.NONE, entry.getKey(), Menu.NONE, entry.getValue().name); + item.setIcon(R.drawable.ic_fluent_number_symbol_24_regular); + UiUtils.insetPopupMenuIcon(ctx, item); + }); + } + + public void updateToolbarLogo(){ + Toolbar toolbar = getToolbar(); + ViewParent parentView = toolbarFrame.getParent(); + if (parentView == toolbar) return; + if (parentView instanceof Toolbar parentToolbar) parentToolbar.removeView(toolbarFrame); + toolbar.addView(toolbarFrame, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + toolbar.setOnClickListener(v->scrollToTop()); + toolbar.setNavigationContentDescription(R.string.back); + toolbar.setContentInsetsAbsolute(0, toolbar.getContentInsetRight()); + + updateSwitcherIcon(pager.getCurrentItem()); + + toolbarShowNewPostsBtn=toolbarFrame.findViewById(R.id.show_new_posts_btn); + toolbarShowNewPostsBtn.setCompoundDrawableTintList(toolbarShowNewPostsBtn.getTextColors()); + if(Build.VERSION.SDK_INT=Build.VERSION_CODES.P && !UiUtils.isEMUI() && !UiUtils.isMagic()) + m.setGroupDividerEnabled(true); } @Override public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ inflater.inflate(R.menu.home, menu); - menu.findItem(R.id.edit_list).setVisible(listMode==ListMode.LIST); - GithubSelfUpdater.UpdateState state=GithubSelfUpdater.UpdateState.NO_UPDATE; - GithubSelfUpdater updater=GithubSelfUpdater.getInstance(); - if(updater!=null) - state=updater.getState(); - if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING) - getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px); - if("debug".equals(BuildConfig.BUILD_TYPE)){ - menu.add(0, 1, 0, "Make a gap"); +// menu.findItem(R.id.overflow).setActionView(overflowActionView); + announcementsAction = menu.findItem(R.id.announcements_action); + settingsAction = menu.findItem(R.id.settings_action); + + updateOverflowMenu(); + } + + private void updateList(List addItems, Map items) { + if (addItems.size() == 0 || getActivity() == null) return; + for (int i = 0; i < addItems.size(); i++) items.put(View.generateViewId(), addItems.get(i)); + updateOverflowMenu(); + } + + private void updateSwitcherMenu() { + Menu switcherMenu = switcherPopup.getMenu(); + switcherMenu.clear(); + timelinesByMenuItem.clear(); + + for (TimelineDefinition tl : timelines) { + int menuItemId = View.generateViewId(); + timelinesByMenuItem.put(menuItemId, tl); + MenuItem item = switcherMenu.add(0, menuItemId, 0, tl.getTitle(getContext())); + item.setIcon(tl.getIcon().iconRes); + } + + UiUtils.enablePopupMenuIcons(getContext(), switcherPopup); + } + + private boolean onSwitcherItemSelected(MenuItem item) { + int id = item.getItemId(); + + Bundle args = new Bundle(); + args.putString("account", accountID); + + if (id == R.id.menu_back) { + switcher.post(() -> switcherPopup.show()); + return true; + } + + TimelineDefinition tl = timelinesByMenuItem.get(id); + if (tl != null) { + for (int i = 0; i < timelines.length; i++) { + if (timelines[i] == tl) { + navigateTo(i); + return true; + } + } + } + + return false; + } + + private void navigateTo(int i) { + navigateTo(i, !reduceMotion); + } + + private void navigateTo(int i, boolean smooth) { + pager.setCurrentItem(i, smooth); + updateSwitcherIcon(i); + } + + @Override + public void showFab() { + if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment l) l.showFab(); + } + + @Override + public void hideFab() { + if (fragments[pager.getCurrentItem()] instanceof BaseStatusListFragment l) l.hideFab(); + } + + @Override + public boolean isScrolling() { + return (fragments[pager.getCurrentItem()] instanceof HasFab fabulous) + && 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) { + // FIXME: make this work again +// elevationOnScrollListener.handleScroll(getContext(), f.isOnTop()); } } @@ -363,389 +531,67 @@ public class HomeTabFragment extends StatusListFragment implements ToolbarDropdo public boolean onOptionsItemSelected(MenuItem item){ Bundle args=new Bundle(); args.putString("account", accountID); - int id=item.getItemId(); - if(id==R.id.settings){ + int id = item.getItemId(); + FollowList list; + Hashtag hashtag; + + if (item.getItemId() == R.id.menu_back) { + getToolbar().post(() -> overflowPopup.show()); + return true; + } else if (id == R.id.settings || id == R.id.settings_action) { Nav.go(getActivity(), SettingsMainFragment.class, args); - }else if(id==R.id.edit_list){ - args.putParcelable("list", Parcels.wrap(currentList)); - Nav.go(getActivity(), EditListFragment.class, args); - }else if(id==1){ - if(data.size()<35){ - Toast.makeText(getActivity(), "Too few posts. Load at least 35", Toast.LENGTH_SHORT).show(); - return true; - } - Status gapStatus=data.get(1); - gapStatus.hasGapAfter=true; - onStatusUpdated(gapStatus); - for(Status s:new ArrayList<>(data.subList(2, 32))){ - removeStatus(s); - } + } 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) { + Nav.go(getActivity(), EditTimelinesFragment.class, args); + } else if ((list = listItems.get(id)) != null) { + args.putString("listID", list.id); + args.putString("listTitle", list.title); + args.putBoolean("listIsExclusive", list.exclusive); + if (list.repliesPolicy != null) args.putInt("repliesPolicy", list.repliesPolicy.ordinal()); + Nav.go(getActivity(), ListTimelineFragment.class, args); + } else if ((hashtag = hashtagsItems.get(id)) != null) { + UiUtils.openHashtagTimeline(getContext(), accountID, hashtag); } return true; } @Override - public void onConfigurationChanged(Configuration newConfig){ - super.onConfigurationChanged(newConfig); - updateToolbarLogo(); - } - - @Override - protected void onShown(){ - super.onShown(); - if(!getArguments().getBoolean("noAutoLoad")){ - if(!loaded && !dataLoading){ - loadData(); - }else if(!dataLoading){ - loadNewPosts(); - } - } - } - - @Override - protected void onHidden(){ - super.onHidden(); - if(!data.isEmpty() && listMode==ListMode.FOLLOWING){ - String topPostID=displayItems.get(Math.max(0, list.getChildAdapterPosition(list.getChildAt(0))-getMainAdapterOffset())).parentID; - if(!topPostID.equals(lastSavedMarkerID)){ - lastSavedMarkerID=topPostID; - new SaveMarkers(topPostID, null) - .setCallback(new Callback<>(){ - @Override - public void onSuccess(TimelineMarkers result){ - } - - @Override - public void onError(ErrorResponse error){ - lastSavedMarkerID=null; - } - }) - .exec(accountID); - } - } - } - - public void onStatusCreated(Status status){ - prependItems(Collections.singletonList(status), true); - } - - private void onFabClick(View v){ - Bundle args=new Bundle(); - args.putString("account", accountID); - Nav.go(getActivity(), ComposeFragment.class, args); - } - - private void loadNewPosts(){ - dataLoading=true; - // The idea here is that we request the timeline such that if there are fewer than `limit` posts, - // 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"; - boolean needCache=listMode==ListMode.FOLLOWING; - loadAdditionalPosts(null, null, 20, sinceID, new Callback<>(){ - @Override - public void onSuccess(List result){ - currentRequest=null; - dataLoading=false; - if(result.isEmpty() || getActivity()==null) - return; - Status last=result.get(result.size()-1); - List toAdd; - if(!data.isEmpty() && last.id.equals(data.get(0).id)){ // This part intersects with the existing one - toAdd=result.subList(0, result.size()-1); // Remove the already known last post - }else{ - result.get(result.size()-1).hasGapAfter=true; - toAdd=result; - } - if(!(toAdd instanceof ArrayList)) - toAdd=new ArrayList<>(toAdd); - Set existingPostIDs=data.stream().map(s->s.id).collect(Collectors.toSet()); - toAdd.removeIf(s->existingPostIDs.contains(s.id)); - if(needCache) - AccountSessionManager.get(accountID).filterStatuses(toAdd, FilterContext.HOME); - if(!toAdd.isEmpty()){ - prependItems(toAdd, true); - showNewPostsButton(); - if(needCache) - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(toAdd, false); - } - } - - @Override - public void onError(ErrorResponse error){ - currentRequest=null; - dataLoading=false; - } - }); - } - - @Override - public void onGapClick(GapStatusDisplayItem.Holder item){ - if(dataLoading) + public void scrollToTop(){ + if (((IsOnTop) fragments[pager.getCurrentItem()]).isOnTop() && + GlobalUserPreferences.doubleTapToSwipe && !newPostsBtnShown) { + int nextPage = (pager.getCurrentItem() + 1) % count; + navigateTo(nextPage); return; - GapStatusDisplayItem gap=item.getItem(); - gap.loading=true; - V.setVisibilityAnimated(item.progress, View.VISIBLE); - V.setVisibilityAnimated(item.text, View.GONE); - dataLoading=true; - boolean needCache=listMode==ListMode.FOLLOWING; - boolean insertBelowGap=!gap.enteredFromTop; - String maxID, minID; - if(gap.enteredFromTop){ - maxID=null; - int gapPos=displayItems.indexOf(gap); - minID=displayItems.get(gapPos+1).parentID; - }else{ - maxID=item.getItemID(); - minID=null; } - loadAdditionalPosts(maxID, minID, 20, null, new Callback<>(){ - @Override - public void onSuccess(List result){ - - currentRequest=null; - dataLoading=false; - if(getActivity()==null) - return; - int gapPos=displayItems.indexOf(gap); - if(gapPos==-1) - return; - if(result.isEmpty()){ - displayItems.remove(gapPos); - adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos); - Status gapStatus=getStatusByID(gap.parentID); - if(gapStatus!=null){ - gapStatus.hasGapAfter=false; - if(needCache) - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(gapStatus), false); - } - }else if(insertBelowGap){ - Set idsBelowGap=new HashSet<>(); - boolean belowGap=false; - int gapPostIndex=0; - for(Status s:data){ - if(belowGap){ - idsBelowGap.add(s.id); - }else if(s.id.equals(gap.parentID)){ - belowGap=true; - s.hasGapAfter=false; - if(needCache) - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(List.of(s), false); - }else{ - gapPostIndex++; - } - } - int endIndex=0; - for(Status s:result){ - endIndex++; - if(idsBelowGap.contains(s.id)) - break; - } - if(endIndex==result.size()){ - result.get(result.size()-1).hasGapAfter=true; - }else{ - result=result.subList(0, endIndex); - } - if(needCache) - AccountSessionManager.get(accountID).filterStatuses(result, FilterContext.HOME); - List targetList=displayItems.subList(gapPos, gapPos+1); // Get a sub-list that contains the gap item - targetList.clear(); // remove the gap item - List insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1); - for(Status s:result){ - if(idsBelowGap.contains(s.id)) - break; - targetList.addAll(buildDisplayItems(s)); - insertedPosts.add(s); - } - if(targetList.isEmpty()){ - // oops. We didn't add new posts, but at least we know there are none. - adapter.notifyItemRemoved(getMainAdapterOffset()+gapPos); - }else{ - adapter.notifyItemChanged(getMainAdapterOffset()+gapPos); - adapter.notifyItemRangeInserted(getMainAdapterOffset()+gapPos+1, targetList.size()-1); - } - if(needCache) - AccountSessionManager.getInstance().getAccount(accountID).getCacheController().putHomeTimeline(insertedPosts, false); - }else{ - Set idsAboveGap=new HashSet<>(); - int gapPostIndex=0; - Status gapPost=null; - for(Status s:data){ - if(s.id.equals(gap.parentID)){ - gapPost=s; - break; - }else{ - idsAboveGap.add(s.id); - gapPostIndex++; - } - } - if(gapPost==null) - return; - boolean needAdjustScroll=false; - int scrollTop=0; - for(int i=0;i targetList=displayItems.subList(gapPos+1, gapPos+1); - List insertedPosts=data.subList(gapPostIndex+1, gapPostIndex+1); - for(int i=result.size()-1;i>=0;i--){ - Status s=result.get(i); - if(idsAboveGap.contains(s.id)) - break; - targetList.addAll(0, buildDisplayItems(s)); - insertedPosts.add(0, s); - } - int addedItemCount=targetList.size(); - boolean gapRemoved=false; - if(insertedPosts.size()=0) - adapter.notifyItemChanged(gapPos); - } - } - }); + ((ScrollableToTop) fragments[pager.getCurrentItem()]).scrollToTop(); } - private void loadAdditionalPosts(String maxID, String minID, int limit, String sinceID, Callback> callback){ - MastodonAPIRequest> req=switch(listMode){ - case FOLLOWING -> new GetHomeTimeline(maxID, minID, limit, sinceID); - case LOCAL -> new GetPublicTimeline(true, false, maxID, minID, limit, sinceID); - case LIST -> new GetListTimeline(currentList.id, maxID, minID, limit, sinceID); - }; - currentRequest=req; - req.setCallback(callback).exec(accountID); - } - - @Override - public void onRefresh(){ - if(currentRequest!=null){ - currentRequest.cancel(); - currentRequest=null; - dataLoading=false; - } - super.onRefresh(); - } - - private void updateToolbarLogo(){ - listsDropdown=new LinearLayout(getActivity()); - listsDropdown.setOnClickListener(this::onListsDropdownClick); - listsDropdown.setBackgroundResource(R.drawable.bg_button_m3_text); - listsDropdown.setAccessibilityDelegate(new View.AccessibilityDelegate(){ - @Override - public void onInitializeAccessibilityNodeInfo(@NonNull View host, @NonNull AccessibilityNodeInfo info){ - super.onInitializeAccessibilityNodeInfo(host, info); - info.setClassName("android.widget.Spinner"); - } - }); - listsDropdownArrow=new FixedAspectRatioImageView(getActivity()); - listsDropdownArrow.setUseHeight(true); - listsDropdownArrow.setImageResource(R.drawable.ic_arrow_drop_down_24px); - listsDropdownArrow.setScaleType(ImageView.ScaleType.CENTER); - listsDropdownArrow.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); - listsDropdown.addView(listsDropdownArrow, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - listsDropdownText=new TextView(getActivity()); - listsDropdownText.setTextAppearance(R.style.action_bar_title); - listsDropdownText.setSingleLine(); - listsDropdownText.setEllipsize(TextUtils.TruncateAt.END); - listsDropdownText.setGravity(Gravity.START | Gravity.CENTER_VERTICAL); - listsDropdownText.setPaddingRelative(V.dp(4), 0, V.dp(16), 0); - listsDropdownText.setText(getCurrentListTitle()); - listsDropdownArrow.setImageTintList(listsDropdownText.getTextColors()); - listsDropdown.setBackgroundTintList(listsDropdownText.getTextColors()); - listsDropdown.addView(listsDropdownText, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - FrameLayout logoWrap=new FrameLayout(getActivity()); - FrameLayout.LayoutParams ddlp=new FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT, Gravity.START); - ddlp.topMargin=ddlp.bottomMargin=V.dp(8); - logoWrap.addView(listsDropdown, ddlp); - - Toolbar toolbar=getToolbar(); - toolbar.addView(logoWrap, new Toolbar.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - toolbar.setContentInsetsRelative(V.dp(16), 0); - } - - private void showNewPostsButton(){ - if(newPostsBtnShown) - return; - newPostsBtnShown=true; - if(currentNewPostsAnim!=null){ - currentNewPostsAnim.cancel(); - } - newPostsBtnWrap.setVisibility(View.VISIBLE); - AnimatorSet set=new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 1f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, 1f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, 1f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, 0f) - ); - set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3)); - set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_decelerate)); - set.addListener(new AnimatorListenerAdapter(){ - @Override - public void onAnimationEnd(Animator animation){ - currentNewPostsAnim=null; - } - }); - currentNewPostsAnim=set; - set.start(); - } - - private void hideNewPostsButton(){ + public void hideNewPostsButton(){ if(!newPostsBtnShown) return; newPostsBtnShown=false; if(currentNewPostsAnim!=null){ currentNewPostsAnim.cancel(); } + timelineTitle.setVisibility(View.VISIBLE); AnimatorSet set=new AnimatorSet(); set.playTogether( - ObjectAnimator.ofFloat(newPostsBtnWrap, View.ALPHA, 0f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_X, .9f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.SCALE_Y, .9f), - ObjectAnimator.ofFloat(newPostsBtnWrap, View.TRANSLATION_Y, V.dp(-56)) + ObjectAnimator.ofFloat(timelineTitle, View.ALPHA, 1f), + ObjectAnimator.ofFloat(timelineTitle, View.SCALE_X, 1f), + ObjectAnimator.ofFloat(timelineTitle, View.SCALE_Y, 1f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 0f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, .8f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, .8f), + ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 0f) ); - set.setDuration(getResources().getInteger(R.integer.m3_sys_motion_duration_medium3)); - set.setInterpolator(AnimationUtils.loadInterpolator(getActivity(), R.interpolator.m3_sys_motion_easing_standard_accelerate)); + set.setDuration(reduceMotion ? 0 : 300); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); set.addListener(new AnimatorListenerAdapter(){ @Override public void onAnimationEnd(Animator animation){ - newPostsBtnWrap.setVisibility(View.GONE); - newPostsBtn.setTranslationY(0); + toolbarShowNewPostsBtn.setVisibility(View.INVISIBLE); + collapsedChevron.setVisibility(View.GONE); currentNewPostsAnim=null; } }); @@ -753,37 +599,64 @@ public class HomeTabFragment extends StatusListFragment implements ToolbarDropdo set.start(); } - private void onNewPostsBtnClick(View v){ + public void showNewPostsButton(){ + if(newPostsBtnShown || pager == null || !timelines[pager.getCurrentItem()].equals(TimelineDefinition.HOME_TIMELINE)) + return; + newPostsBtnShown=true; + if(currentNewPostsAnim!=null){ + currentNewPostsAnim.cancel(); + } + toolbarShowNewPostsBtn.setVisibility(View.VISIBLE); + collapsedChevron.setVisibility(View.VISIBLE); + AnimatorSet set=new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofFloat(timelineTitle, View.ALPHA, 0f), + ObjectAnimator.ofFloat(timelineTitle, View.SCALE_X, .8f), + ObjectAnimator.ofFloat(timelineTitle, View.SCALE_Y, .8f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.ALPHA, 1f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_X, 1f), + ObjectAnimator.ofFloat(toolbarShowNewPostsBtn, View.SCALE_Y, 1f), + ObjectAnimator.ofFloat(collapsedChevron, View.ALPHA, 1f) + ); + set.setDuration(reduceMotion ? 0 : 300); + set.setInterpolator(CubicBezierInterpolator.DEFAULT); + set.addListener(new AnimatorListenerAdapter(){ + @Override + public void onAnimationEnd(Animator animation){ + timelineTitle.setVisibility(View.GONE); + currentNewPostsAnim=null; + } + }); + currentNewPostsAnim=set; + set.start(); + } + + public boolean isNewPostsBtnShown() { + return newPostsBtnShown; + } + + private void onNewPostsBtnClick(View view) { if(newPostsBtnShown){ + scrollToTop(); hideNewPostsButton(); - smoothScrollRecyclerViewToTop(list); } } - private void onListsDropdownClick(View v){ - listsDropdownArrow.animate().rotation(-180f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); - dropdownController.show(dropdownMainMenuController); - AccountSessionManager.get(accountID).getCacheController().reloadLists(new Callback<>(){ - @Override - public void onSuccess(java.util.List result){ - lists=result; - } - - @Override - public void onError(ErrorResponse error){} - }); - } - @Override - public void onDestroyView(){ - super.onDestroyView(); - donationBanner=null; - donationBannerDismissing=false; + public void onFragmentResult(int reqCode, boolean success, Bundle result){ + if (reqCode == ANNOUNCEMENTS_RESULT && success) { + announcementsBadged = false; + announcements.setVisible(true); + announcementsAction.setVisible(false); + } } private void updateUpdateState(GithubSelfUpdater.UpdateState state){ - if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING) - getToolbar().getMenu().findItem(R.id.settings).setIcon(R.drawable.ic_settings_updateready_24px); + if(state!=GithubSelfUpdater.UpdateState.NO_UPDATE && state!=GithubSelfUpdater.UpdateState.CHECKING) { + settingsBadged = true; + settingsAction.setVisible(true); + settings.setVisible(false); + } } @Subscribe @@ -791,223 +664,142 @@ public class HomeTabFragment extends StatusListFragment implements ToolbarDropdo updateUpdateState(ev.state); } +// @Override +// public boolean onBackPressed(){ +// if(pager.getCurrentItem() > 0){ +// navigateTo(0); +// return true; +// } +// return false; +// } + + @Override + public void onDestroyView(){ + super.onDestroyView(); + if (overflowPopup != null) { + overflowPopup.dismiss(); + overflowPopup = null; + } + if (switcherPopup != null) { + switcherPopup.dismiss(); + switcherPopup = null; + } + if(GithubSelfUpdater.needSelfUpdating()){ + E.unregister(this); + } + } + + @Override + protected void onShown() { + super.onShown(); + Object timelines = AccountSessionManager.get(accountID).getLocalPreferences().timelines; + if (timelines != null && timelinesList!= timelines) UiUtils.restartApp(); + } + + @Override + public void onViewStateRestored(Bundle savedInstanceState) { + super.onViewStateRestored(savedInstanceState); + if (savedInstanceState == null) return; + navigateTo(savedInstanceState.getInt("selectedTab"), false); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putInt("selectedTab", pager.getCurrentItem()); + } + @Subscribe - public void onDismissDonationCampaignBanner(DismissDonationCampaignBannerEvent ev){ - if(currentDonationCampaign!=null && ev.campaignID.equals(currentDonationCampaign.id)){ - dismissDonationBanner(); - } - } - - @Override - protected boolean shouldRemoveAccountPostsWhenUnfollowing(){ - return true; - } - - @Override - public Toolbar getToolbar(){ - return super.getToolbar(); - } - - @Override - public void onDropdownWillDismiss(){ - listsDropdownArrow.animate().rotation(0f).setDuration(150).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); - - } - - @Override - public void onDropdownDismissed(){ - - } - - @Override - public void reload(){ - if(currentRequest!=null){ - currentRequest.cancel(); - currentRequest=null; - } - refreshing=true; - showProgress(); - loadData(); - listsDropdownText.setText(getCurrentListTitle()); - invalidateOptionsMenu(); - } - - @Override - protected RecyclerView.Adapter getAdapter(){ - mergeAdapter=new MergeRecyclerAdapter(); - mergeAdapter.addAdapter(super.getAdapter()); - return mergeAdapter; - } - - @Override - protected void onDataLoaded(List d, boolean more){ - if(refreshing){ - if(listMode==ListMode.LOCAL){ - localTimelineBannerHelper.maybeAddBanner(list, mergeAdapter); - localTimelineBannerHelper.onBannerBecameVisible(); - }else{ - localTimelineBannerHelper.removeBanner(mergeAdapter); - } - } - super.onDataLoaded(d, more); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data){ - if(requestCode==DONATION_RESULT){ - if(donationSheet!=null) - donationSheet.dismissWithoutAnimation(); - if(resultCode==Activity.RESULT_OK){ - new DonationSuccessfulSheet(getActivity(), accountID, data.getStringExtra("postText")).showWithoutAnimation(); - } - } - } - - @Override - public void scrollToTop(){ - if(list.getChildCount()==0) - return; - scrollWrapper.smoothScrollTo(0, 0); - View topChild=list.getLayoutManager().getChildAt(0); - if(list.getChildAdapterPosition(topChild)==0){ - if(topChild.getTop()==list.getPaddingTop() && scrollBackItemID!=null && System.currentTimeMillis()-scrollBackTime<5*60_000){ - int indexWithinPost=0; - for(int i=0;i=Build.VERSION_CODES.S) - UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_THUD); - return; - } - indexWithinPost++; - } - } - }else{ - smoothScrollRecyclerViewToTop(list); - return; - } - }else if(list.getChildViewHolder(topChild) instanceof StatusDisplayItem.Holder itemHolder){ - int postIndex; - String id=itemHolder.getItemID(); - for(postIndex=0;postIndex1){ - scrollBackItemID=id; - scrollBackItemIndex=0; - for(StatusDisplayItem item:displayItems){ - if(item.parentID.equals(id)){ - if(item==itemHolder.getItem()) - break; - scrollBackItemIndex++; - } - } - scrollBackItemOffset=topChild.getTop(); - scrollBackTime=System.currentTimeMillis(); - }else{ - scrollBackItemID=null; - } - } - smoothScrollRecyclerViewToTop(list); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S) - UiUtils.playVibrationEffectIfSupported(getActivity(), VibrationEffect.Composition.PRIMITIVE_QUICK_RISE); - } - - private String getCurrentListTitle(){ - return switch(listMode){ - case FOLLOWING -> getString(R.string.timeline_following); - case LOCAL -> getString(R.string.local_timeline); - case LIST -> currentList.title; - }; - } - - private void showDonationBanner(DonationCampaign campaign){ - if(getActivity()==null) - return; - currentDonationCampaign=campaign; - if(donationBanner==null){ - ViewStub stub=contentView.findViewById(R.id.donation_banner); - donationBanner=stub.inflate(); - donationBanner.findViewById(R.id.banner_dismiss).setOnClickListener(v->{ - AccountSessionManager.getInstance().markDonationCampaignAsDismissed(currentDonationCampaign.id); - dismissDonationBanner(); - }); - donationBanner.setOnClickListener(v->openDonationSheet()); - }else{ - donationBanner.setVisibility(View.VISIBLE); - } - TextView text=donationBanner.findViewById(R.id.banner_text); - SpannableStringBuilder ssb=new SpannableStringBuilder(campaign.bannerMessage); - if(!campaign.bannerMessage.endsWith("\n")) - ssb.append(' '); - int start=ssb.length(); - ssb.append(campaign.bannerButtonText.trim()); - ssb.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.masterialDark_colorGoldenrodContainer, getActivity().getTheme())), start, ssb.length(), 0); - ssb.setSpan(new UnderlineSpan(), start, ssb.length(), 0); - ssb.setSpan(new TypefaceSpan("sans-serif-medium"), start, ssb.length(), 0); - text.setText(ssb); - donationBanner.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener(){ - @Override - public boolean onPreDraw(){ - donationBanner.getViewTreeObserver().removeOnPreDrawListener(this); - - AnimatorSet set=new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight(), 0), - ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, -donationBanner.getHeight()) - ); - set.setDuration(250); - set.setInterpolator(CubicBezierInterpolator.DEFAULT); - set.start(); - - return true; - } + public void onHashtagUpdatedEvent(HashtagUpdatedEvent event) { + handleListEvent(hashtagsItems, h -> h.name.equalsIgnoreCase(event.name), event.following, () -> { + Hashtag hashtag = new Hashtag(); + hashtag.name = event.name; + hashtag.following = true; + return hashtag; }); } - private void dismissDonationBanner(){ - if(donationBanner==null || donationBannerDismissing) - return; - AnimatorSet set=new AnimatorSet(); - set.playTogether( - ObjectAnimator.ofFloat(donationBanner, View.TRANSLATION_Y, donationBanner.getHeight()), - ObjectAnimator.ofFloat(fab, View.TRANSLATION_Y, 0) - ); - set.setDuration(250); - set.setInterpolator(CubicBezierInterpolator.DEFAULT); - set.addListener(new AnimatorListenerAdapter(){ - @Override - public void onAnimationEnd(Animator animation){ - donationBanner.setVisibility(View.GONE); - donationBannerDismissing=false; - } + @Subscribe + public void onListDeletedEvent(ListDeletedEvent event) { + handleListEvent(listItems, l -> l.id.equals(event.listID), false, null); + } + + @Subscribe + public void onListCreatedEvent(ListCreatedEvent event) { + handleListEvent(listItems, l -> l.id.equals(event.list.id), true, () -> { + FollowList list = new FollowList(); + list.id = event.list.id; + list.title = event.list.title; + list.repliesPolicy = event.list.repliesPolicy; + return list; }); - donationBannerDismissing=true; - set.start(); - currentDonationCampaign=null; } - private void openDonationSheet(){ - donationSheet=new DonationSheet(getActivity(), currentDonationCampaign, accountID, intent->startActivityForResult(intent, DONATION_RESULT)); - donationSheet.setOnDismissListener(dialog->donationSheet=null); - donationSheet.show(); + private void handleListEvent( + Map existingThings, + Predicate matchExisting, + boolean shouldBeInList, + Supplier makeNewThing + ) { + Optional> existingThing = existingThings.entrySet().stream() + .filter(e -> matchExisting.test(e.getValue())).findFirst(); + if (shouldBeInList) { + existingThings.put(existingThing.isPresent() + ? existingThing.get().getKey() : View.generateViewId(), makeNewThing.get()); + updateOverflowMenu(); + } else if (existingThing.isPresent() && !shouldBeInList) { + existingThings.remove(existingThing.get().getKey()); + updateOverflowMenu(); + } } - private enum ListMode{ - FOLLOWING, - LOCAL, - LIST +// public void rebuildAllDisplayItems(){ +// displayItems.clear(); +// for(T item:data){ +// displayItems.addAll(buildDisplayItems(item)); +// } +// adapter.notifyDataSetChanged(); +// } + + public Collection getHashtags() { + return hashtagsItems.values(); + } + + public Fragment getCurrentFragment() { + return fragments[pager.getCurrentItem()]; + } + + public ImageButton getFab() { + return fab; + } + + @Override + public void onProvideAssistContent(AssistContent assistContent) { + callFragmentToProvideAssistContent(fragments[pager.getCurrentItem()], assistContent); + } + + private class HomePagerAdapter extends RecyclerView.Adapter { + @NonNull + @Override + public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + FrameLayout tabView = tabViews[viewType % getItemCount()]; + ViewGroup tabParent = (ViewGroup) tabView.getParent(); + if (tabParent != null) tabParent.removeView(tabView); + tabView.setVisibility(View.VISIBLE); + return new SimpleViewHolder(tabView); + } + + @Override + public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){} + + @Override + public int getItemCount(){ + return count; + } + + @Override + public int getItemViewType(int position){ + return position; + } } }