Merge branch 'merge-upstream'
# Conflicts: # mastodon/build.gradle # mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java # mastodon/src/main/java/org/joinmastodon/android/ExternalShareActivity.java # mastodon/src/main/java/org/joinmastodon/android/GlobalUserPreferences.java # mastodon/src/main/java/org/joinmastodon/android/MainActivity.java # mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java # mastodon/src/main/java/org/joinmastodon/android/api/MastodonAPIController.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/AddAccountsToList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/CreateList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/DeleteList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/GetList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/GetLists.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/RemoveAccountsFromList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/lists/UpdateList.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/statuses/GetStatusEditHistory.java # mastodon/src/main/java/org/joinmastodon/android/api/requests/timelines/GetPublicTimeline.java # mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java # mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java # mastodon/src/main/java/org/joinmastodon/android/events/ListDeletedEvent.java # mastodon/src/main/java/org/joinmastodon/android/events/ListUpdatedCreatedEvent.java # mastodon/src/main/java/org/joinmastodon/android/fragments/BaseStatusListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ComposeFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/CustomLocalTimelineFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/EditTimelinesFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTabFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ListsFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ProfileFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/ThreadFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/account_list/PaginatedAccountListFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsAboutAppFragment.java # mastodon/src/main/java/org/joinmastodon/android/fragments/settings/SettingsMainFragment.java # mastodon/src/main/java/org/joinmastodon/android/model/Status.java # mastodon/src/main/java/org/joinmastodon/android/model/TimelineDefinition.java # mastodon/src/main/java/org/joinmastodon/android/model/viewmodel/AccountViewModel.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/ExtendedFooterStatusDisplayItem.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/LinkCardStatusDisplayItem.java # mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java # mastodon/src/main/java/org/joinmastodon/android/ui/photoviewer/PhotoViewer.java # mastodon/src/main/java/org/joinmastodon/android/ui/sheets/AccountSwitcherSheet.java # mastodon/src/main/java/org/joinmastodon/android/ui/text/HtmlParser.java # mastodon/src/main/java/org/joinmastodon/android/ui/text/LinkSpan.java # mastodon/src/main/java/org/joinmastodon/android/ui/utils/ActionModeHelper.java # mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java # mastodon/src/main/java/org/joinmastodon/android/ui/viewcontrollers/ComposeLanguageAlertViewController.java # mastodon/src/main/java/org/joinmastodon/android/ui/viewholders/AccountViewHolder.java # mastodon/src/main/java/org/joinmastodon/android/ui/views/ListEditor.java # mastodon/src/main/res/drawable/fg_link_card.xml # mastodon/src/main/res/drawable/ic_arrow_drop_down_24px.xml # mastodon/src/main/res/drawable/ic_arrow_right_24px.xml # mastodon/src/main/res/drawable/ic_arrow_upward_24px.xml # mastodon/src/main/res/drawable/ic_bookmark_fill1_24px.xml # mastodon/src/main/res/drawable/ic_boost_24px.xml # mastodon/src/main/res/drawable/ic_boost_private.xml # mastodon/src/main/res/drawable/ic_help_24px.xml # mastodon/src/main/res/drawable/ic_info_fill1_24px.xml # mastodon/src/main/res/drawable/ic_m3_cancel.xml # mastodon/src/main/res/layout/display_item_extended_footer.xml # mastodon/src/main/res/layout/display_item_link_card.xml # mastodon/src/main/res/layout/fragment_onboarding_follow_suggestions.xml # mastodon/src/main/res/layout/fragment_onboarding_profile_setup.xml # mastodon/src/main/res/layout/home_toolbar.xml # mastodon/src/main/res/layout/item_discover_account.xml # mastodon/src/main/res/layout/item_generic_list_content.xml # mastodon/src/main/res/menu/post.xml # mastodon/src/main/res/menu/profile.xml # mastodon/src/main/res/values-ar-rSA/strings.xml # mastodon/src/main/res/values-be-rBY/strings.xml # mastodon/src/main/res/values-ca-rES/strings.xml # mastodon/src/main/res/values-cs-rCZ/strings.xml # mastodon/src/main/res/values-da-rDK/strings.xml # mastodon/src/main/res/values-eu-rES/strings.xml # mastodon/src/main/res/values-fa-rIR/strings.xml # mastodon/src/main/res/values-fi-rFI/strings.xml # mastodon/src/main/res/values-fil-rPH/strings.xml # mastodon/src/main/res/values-fr-rFR/strings.xml # mastodon/src/main/res/values-gd-rGB/strings.xml # mastodon/src/main/res/values-gl-rES/strings.xml # mastodon/src/main/res/values-hu-rHU/strings.xml # mastodon/src/main/res/values-hy-rAM/strings.xml # mastodon/src/main/res/values-in-rID/strings.xml # mastodon/src/main/res/values-is-rIS/strings.xml # mastodon/src/main/res/values-it-rIT/strings.xml # mastodon/src/main/res/values-ja-rJP/strings.xml # mastodon/src/main/res/values-kab/strings.xml # mastodon/src/main/res/values-ko-rKR/strings.xml # mastodon/src/main/res/values-nl-rNL/strings.xml # mastodon/src/main/res/values-pl-rPL/strings.xml # mastodon/src/main/res/values-pt-rBR/strings.xml # mastodon/src/main/res/values-pt-rPT/strings.xml # mastodon/src/main/res/values-ro-rRO/strings.xml # mastodon/src/main/res/values-ru-rRU/strings.xml # mastodon/src/main/res/values-sl-rSI/strings.xml # mastodon/src/main/res/values-sv-rSE/strings.xml # mastodon/src/main/res/values-tr-rTR/strings.xml # mastodon/src/main/res/values-uk-rUA/strings.xml # mastodon/src/main/res/values-zh-rCN/strings.xml # mastodon/src/main/res/values/attrs.xml # mastodon/src/main/res/values/palettes.xml # mastodon/src/main/res/values/strings.xml # mastodon/src/main/res/values/styles.xml
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountLists;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AddAccountToListsFragment extends BaseSettingsFragment<FollowList>{
|
||||
private Account account;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.add_user_to_list_title);
|
||||
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
AccountSessionManager.get(accountID).getCacheController().getLists(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> allLists){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
loadAccountLists(allLists);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void loadAccountLists(final List<FollowList> allLists){
|
||||
currentRequest=new GetAccountLists(account.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
Set<String> lists=result.stream().map(l->l.id).collect(Collectors.toSet());
|
||||
onDataLoaded(allLists.stream()
|
||||
.map(l->new CheckableListItem<>(l.title, null, CheckableListItem.Style.CHECKBOX, lists.contains(l.id),
|
||||
R.drawable.ic_list_alt_24px, AddAccountToListsFragment.this::onItemClick, l))
|
||||
.collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
TextView topText=new TextView(getActivity());
|
||||
topText.setTextAppearance(R.style.m3_body_medium);
|
||||
topText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
topText.setPadding(V.dp(16), V.dp(8), V.dp(16), V.dp(8));
|
||||
topText.setText(getString(R.string.manage_user_lists, account.getDisplayUsername()));
|
||||
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(topText));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void onItemClick(CheckableListItem<FollowList> item){
|
||||
boolean add=!item.checked;
|
||||
ResultlessMastodonAPIRequest req=add ? new AddAccountsToList(item.parentObject.id, Set.of(account.id)) : new RemoveAccountsFromList(item.parentObject.id, Set.of(account.id));
|
||||
req.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
item.checked=add;
|
||||
rebindItem(item);
|
||||
if(add){
|
||||
E.post(new AccountAddedToListEvent(accountID, item.parentObject.id, account));
|
||||
}else{
|
||||
E.post(new AccountRemovedFromListEvent(accountID, item.parentObject.id, account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, false)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AvatarPileListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.views.FloatingHintEditTextLayout;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
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.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public abstract class BaseEditListFragment extends BaseSettingsFragment<Void>{
|
||||
protected FollowList followList;
|
||||
protected AvatarPileListItem<Void> membersItem;
|
||||
protected CheckableListItem<Void> exclusiveItem;
|
||||
protected FloatingHintEditTextLayout titleEditLayout;
|
||||
protected EditText titleEdit;
|
||||
protected Spinner showRepliesSpinner;
|
||||
private APIRequest<?> getMembersRequest;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
|
||||
membersItem=new AvatarPileListItem<>(getString(R.string.list_members), null, List.of(), 0, i->onMembersClick(), null, false);
|
||||
List<ListItem<Void>> items=new ArrayList<>();
|
||||
if(followList!=null){
|
||||
items.add(membersItem);
|
||||
}
|
||||
exclusiveItem=new CheckableListItem<>(R.string.list_exclusive, R.string.list_exclusive_subtitle, CheckableListItem.Style.SWITCH, followList!=null && followList.exclusive, this::toggleCheckableItem);
|
||||
items.add(exclusiveItem);
|
||||
onDataLoaded(items);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(getMembersRequest!=null)
|
||||
getMembersRequest.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
LinearLayout topView=new LinearLayout(getActivity());
|
||||
topView.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
titleEditLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_edit_text, topView, false);
|
||||
titleEdit=titleEditLayout.findViewById(R.id.edit);
|
||||
titleEdit.setHint(R.string.list_name);
|
||||
titleEditLayout.updateHint();
|
||||
if(followList!=null)
|
||||
titleEdit.setText(followList.title);
|
||||
topView.addView(titleEditLayout);
|
||||
|
||||
FloatingHintEditTextLayout showRepliesLayout=(FloatingHintEditTextLayout) getActivity().getLayoutInflater().inflate(R.layout.floating_hint_spinner, topView, false);
|
||||
showRepliesSpinner=showRepliesLayout.findViewById(R.id.spinner);
|
||||
showRepliesLayout.setHint(R.string.list_show_replies_to);
|
||||
topView.addView(showRepliesLayout);
|
||||
ArrayAdapter<String> spinnerAdapter=new ArrayAdapter<>(getActivity(), R.layout.item_spinner, List.of(
|
||||
getString(R.string.list_replies_no_one),
|
||||
getString(R.string.list_replies_members),
|
||||
getString(R.string.list_replies_anyone)
|
||||
));
|
||||
showRepliesSpinner.setAdapter(spinnerAdapter);
|
||||
showRepliesSpinner.setSelection(switch(followList!=null ? followList.repliesPolicy : FollowList.RepliesPolicy.LIST){
|
||||
case FOLLOWED -> 2;
|
||||
case LIST -> 1;
|
||||
case NONE -> 0;
|
||||
});
|
||||
ViewGroup.MarginLayoutParams llp=(ViewGroup.MarginLayoutParams)showRepliesLayout.getLabel().getLayoutParams();
|
||||
llp.setMarginStart(llp.getMarginStart()+V.dp(16));
|
||||
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(topView));
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
return adapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int indexOfItemsAdapter(){
|
||||
return 1;
|
||||
}
|
||||
|
||||
protected void doDeleteList(){
|
||||
new DeleteList(followList.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
AccountSessionManager.get(accountID).getCacheController().deleteList(followList.id);
|
||||
E.post(new ListDeletedEvent(accountID, followList.id));
|
||||
Nav.finish(BaseEditListFragment.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void onMembersClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
Nav.go(getActivity(), ListMembersFragment.class, args);
|
||||
}
|
||||
|
||||
protected void loadMembers(){
|
||||
getMembersRequest=new GetListAccounts(followList.id, null, 3)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
getMembersRequest=null;
|
||||
membersItem.avatars=new ArrayList<>();
|
||||
for(int i=0;i<Math.min(3, result.size());i++){
|
||||
Account acc=result.get(i);
|
||||
membersItem.avatars.add(new UrlImageLoaderRequest(acc.avatarStatic, V.dp(32), V.dp(32)));
|
||||
}
|
||||
rebindItem(membersItem);
|
||||
imgLoader.updateImages();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
getMembersRequest=null;
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected FollowList.RepliesPolicy getSelectedRepliesPolicy(){
|
||||
return switch(showRepliesSpinner.getSelectedItemPosition()){
|
||||
case 0 -> FollowList.RepliesPolicy.NONE;
|
||||
case 1 -> FollowList.RepliesPolicy.LIST;
|
||||
case 2 -> FollowList.RepliesPolicy.FOLLOWED;
|
||||
default -> throw new IllegalStateException("Unexpected value: "+showRepliesSpinner.getSelectedItemPosition());
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
import org.joinmastodon.android.api.session.AccountLocalPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
@@ -40,6 +42,8 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.NonMutualPreReplySheet;
|
||||
import org.joinmastodon.android.ui.OldPostPreReplySheet;
|
||||
import org.joinmastodon.android.ui.displayitems.AccountStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
@@ -63,6 +67,8 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
@@ -145,6 +151,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
for(T s:items){
|
||||
displayItems.addAll(buildDisplayItems(s));
|
||||
}
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -166,6 +173,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
if(notify)
|
||||
adapter.notifyItemRangeInserted(0, offset);
|
||||
loadRelationships(items.stream().map(DisplayItemsParent::getAccountID).filter(Objects::nonNull).collect(Collectors.toSet()));
|
||||
return offset;
|
||||
}
|
||||
|
||||
@@ -222,7 +230,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void openPhotoViewer(String parentID, Status _status, int attachmentIndex, MediaGridStatusDisplayItem.Holder gridHolder){
|
||||
final Status status=_status.getContentStatus();
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||
private MediaAttachmentViewController transitioningHolder;
|
||||
|
||||
@Override
|
||||
@@ -288,6 +296,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
@Override
|
||||
public void photoViewerDismissed(){
|
||||
currentPhotoViewer=null;
|
||||
gridHolder.itemView.setHasTransientState(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -299,12 +308,13 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
return gridHolder.getViewController(index);
|
||||
}
|
||||
});
|
||||
gridHolder.itemView.setHasTransientState(true);
|
||||
}
|
||||
|
||||
|
||||
public void openPreviewlessMediaPhotoViewer(String parentID, Status _status, int attachmentIndex, PreviewlessMediaGridStatusDisplayItem.Holder gridHolder){
|
||||
final Status status=_status.getContentStatus();
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, new PhotoViewer.Listener(){
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), status.mediaAttachments, attachmentIndex, status, accountID, new PhotoViewer.Listener(){
|
||||
private PreviewlessMediaAttachmentViewController transitioningHolder;
|
||||
|
||||
@Override
|
||||
@@ -775,6 +785,9 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
|
||||
protected void loadRelationships(Set<String> ids){
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
ids=ids.stream().filter(id->!relationships.containsKey(id)).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
// TODO somehow manage these and cancel outstanding requests on refresh
|
||||
@@ -1066,6 +1079,26 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void maybeShowPreReplySheet(Status status, Runnable proceed){
|
||||
Relationship rel=getRelationship(status.account.id);
|
||||
if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, status.account, accountID) &&
|
||||
!status.account.id.equals(AccountSessionManager.get(accountID).self.id) && rel!=null && !rel.followedBy && status.account.followingCount>=1){
|
||||
new NonMutualPreReplySheet(getActivity(), notAgain->{
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.NON_MUTUAL, notAgain ? null : status.account, accountID);
|
||||
proceed.run();
|
||||
}, status.account, accountID).show();
|
||||
}else if(!GlobalUserPreferences.isOptedOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null) &&
|
||||
status.createdAt.isBefore(Instant.now().minus(90, ChronoUnit.DAYS))){
|
||||
new OldPostPreReplySheet(getActivity(), notAgain->{
|
||||
if(notAgain)
|
||||
GlobalUserPreferences.optOutOfPreReplySheet(GlobalUserPreferences.PreReplySheetType.OLD_POST, null, null);
|
||||
proceed.run();
|
||||
}, status).show();
|
||||
}else{
|
||||
proceed.run();
|
||||
}
|
||||
}
|
||||
|
||||
protected void onModifyItemViewHolder(BindableViewHolder<StatusDisplayItem> holder){}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -7,6 +7,9 @@ import static org.joinmastodon.android.api.requests.statuses.CreateStatus.getDra
|
||||
|
||||
import android.Manifest;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.DatePickerDialog;
|
||||
@@ -75,7 +78,7 @@ import org.joinmastodon.android.events.ScheduledStatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||
import org.joinmastodon.android.events.StatusUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.ComposeAccountSearchFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.AccountSearchFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.ContentType;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
@@ -135,12 +138,14 @@ import java.util.stream.Collectors;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.fragments.CustomTransitionsFragment;
|
||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID {
|
||||
public class ComposeFragment extends MastodonToolbarFragment implements OnBackPressedListener, ComposeEditText.SelectionListener, HasAccountID, CustomTransitionsFragment {
|
||||
|
||||
private static final int MEDIA_RESULT=717;
|
||||
public static final int IMAGE_DESCRIPTION_RESULT=363;
|
||||
@@ -527,7 +532,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
public void onLaunchAccountSearch(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), ComposeAccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
Nav.goForResult(getActivity(), AccountSearchFragment.class, args, AUTOCOMPLETE_ACCOUNT_RESULT, ComposeFragment.this);
|
||||
}
|
||||
});
|
||||
View autocompleteView=autocompleteViewController.getView();
|
||||
@@ -1806,8 +1811,26 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
return new String[]{"image/jpeg", "image/gif", "image/png", "video/mp4"};
|
||||
}
|
||||
|
||||
private String sanitizeMediaDescription(String description){
|
||||
if(description == null){
|
||||
return null;
|
||||
}
|
||||
|
||||
// The Gboard android keyboard attaches this text whenever the user
|
||||
// pastes something from the keyboard's suggestion bar.
|
||||
// Due to different end user locales, the exact text may vary, but at
|
||||
// least in version 13.4.08, all of the translations contained the
|
||||
// string "Gboard".
|
||||
if (description.contains("Gboard")){
|
||||
return null;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onAddMediaAttachmentFromEditText(Uri uri, String description){
|
||||
description = sanitizeMediaDescription(description);
|
||||
return mediaViewController.addMediaAttachment(uri, description);
|
||||
}
|
||||
|
||||
@@ -1850,6 +1873,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Editable e=mainEditText.getText();
|
||||
int start=e.getSpanStart(currentAutocompleteSpan);
|
||||
int end=e.getSpanEnd(currentAutocompleteSpan);
|
||||
if(start==-1 || end==-1)
|
||||
return;
|
||||
e.replace(start, end, text+" ");
|
||||
finishAutocomplete();
|
||||
InputConnection conn=mainEditText.getCurrentInputConnection();
|
||||
@@ -1918,4 +1943,35 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
languageButton.setText(opt.language.getLanguageName());
|
||||
languageButton.setContentDescription(getActivity().getString(R.string.sk_post_language, opt.language.getDefaultName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateEnterTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
if(getArguments().getBoolean("fromThreadFragment")){
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_Y, V.dp(200), 0)
|
||||
);
|
||||
}else{
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0f, 1f),
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100), 0)
|
||||
);
|
||||
}
|
||||
anim.setDuration(300);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Animator onCreateExitTransition(View prev, View container){
|
||||
AnimatorSet anim=new AnimatorSet();
|
||||
anim.playTogether(
|
||||
ObjectAnimator.ofFloat(container, View.TRANSLATION_X, V.dp(100)),
|
||||
ObjectAnimator.ofFloat(container, View.ALPHA, 0)
|
||||
);
|
||||
anim.setDuration(200);
|
||||
anim.setInterpolator(CubicBezierInterpolator.DEFAULT);
|
||||
return anim;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,7 @@ 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.text.SpannableStringBuilder;
|
||||
import android.text.style.BulletSpan;
|
||||
import android.util.Log;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -135,20 +132,9 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
@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)
|
||||
.setMessage(UiUtils.fixBulletListInString(themeWrapper, R.string.alt_text_help))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
@@ -185,7 +171,7 @@ public class ComposeImageDescriptionFragment extends MastodonToolbarFragment imp
|
||||
fakeAttachment.meta.width=width;
|
||||
fakeAttachment.meta.height=height;
|
||||
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, new PhotoViewer.Listener(){
|
||||
photoViewer=new PhotoViewer(getActivity(), Collections.singletonList(fakeAttachment), 0, null, accountID, new PhotoViewer.Listener(){
|
||||
@Override
|
||||
public void setPhotoViewVisibility(int index, boolean visible){
|
||||
image.setAlpha(visible ? 1f : 0f);
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.content.res.TypedArray;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Gravity;
|
||||
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.ViewStub;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AddNewListMembersFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
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.ui.views.CurlyArrowEmptyView;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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.OnBackPressedListener;
|
||||
import me.grishka.appkit.fragments.WindowInsetsAwareFragment;
|
||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class CreateListAddMembersFragment extends BaseAccountListFragment implements OnBackPressedListener, AddNewListMembersFragment.Listener{
|
||||
private FollowList followList;
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FragmentRootLinearLayout rootView;
|
||||
private FrameLayout searchFragmentContainer;
|
||||
private FrameLayout fragmentContentWrap;
|
||||
private AddNewListMembersFragment searchFragment;
|
||||
private WindowInsets lastInsets;
|
||||
private boolean dismissingSearchFragment;
|
||||
private HashSet<String> accountIDsInList=new HashSet<>();
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_list_members);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 2, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
setEmptyText(R.string.list_no_members);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
if(savedInstanceState!=null || getArguments().getBoolean("needLoadMembers", false)){
|
||||
loadData();
|
||||
}else{
|
||||
onDataLoaded(List.of());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetListAccounts(followList.id, null, 0)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
for(Account acc:result)
|
||||
accountIDsInList.add(acc.id);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()));
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){
|
||||
View view=super.onCreateView(inflater, container, savedInstanceState);
|
||||
FrameLayout wrapper=new FrameLayout(getActivity());
|
||||
wrapper.addView(view);
|
||||
rootView=(FragmentRootLinearLayout) view;
|
||||
fragmentContentWrap=wrapper;
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.done);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastInsets=insets;
|
||||
if(searchFragment!=null)
|
||||
searchFragment.onApplyWindowInsets(insets);
|
||||
insets=UiUtils.applyBottomInsetToFixedView(buttonBar, insets);
|
||||
rootView.dispatchApplyWindowInsets(insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
MenuItem item=menu.add(R.string.add_list_member);
|
||||
item.setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(searchFragmentContainer!=null)
|
||||
return true;
|
||||
|
||||
searchFragmentContainer=new FrameLayout(getActivity());
|
||||
searchFragmentContainer.setId(R.id.search_fragment);
|
||||
fragmentContentWrap.addView(searchFragmentContainer);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("_can_go_back", true);
|
||||
searchFragment=new AddNewListMembersFragment(this);
|
||||
searchFragment.setArguments(args);
|
||||
getChildFragmentManager().beginTransaction().add(R.id.search_fragment, searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
if(lastInsets!=null)
|
||||
searchFragment.onApplyWindowInsets(lastInsets);
|
||||
searchFragmentContainer.setTranslationX(V.dp(100));
|
||||
searchFragmentContainer.setAlpha(0f);
|
||||
searchFragmentContainer.animate().translationX(0).alpha(1).setDuration(300).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
rootView.setVisibility(View.GONE);
|
||||
}).start();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void initializeEmptyView(View contentView){
|
||||
ViewStub emptyStub=contentView.findViewById(R.id.empty);
|
||||
emptyStub.setLayoutResource(R.layout.empty_with_arrow);
|
||||
super.initializeEmptyView(contentView);
|
||||
TextView emptySecondary=contentView.findViewById(R.id.empty_text_secondary);
|
||||
emptySecondary.setText(R.string.list_find_users);
|
||||
CurlyArrowEmptyView arrowView=(CurlyArrowEmptyView) emptyView;
|
||||
arrowView.setGravityAndOffsets(Gravity.TOP | Gravity.END, 24, 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStatusBarColor(int color){
|
||||
rootView.setStatusBarColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setNavigationBarColor(int color){
|
||||
rootView.setNavigationBarColor(color);
|
||||
}
|
||||
|
||||
private void dismissSearchFragment(){
|
||||
if(searchFragment==null || dismissingSearchFragment)
|
||||
return;
|
||||
dismissingSearchFragment=true;
|
||||
rootView.setVisibility(View.VISIBLE);
|
||||
searchFragmentContainer.animate().translationX(V.dp(100)).alpha(0).setDuration(200).withLayer().setInterpolator(CubicBezierInterpolator.DEFAULT).withEndAction(()->{
|
||||
getChildFragmentManager().beginTransaction().remove(searchFragment).commit();
|
||||
getChildFragmentManager().executePendingTransactions();
|
||||
fragmentContentWrap.removeView(searchFragmentContainer);
|
||||
searchFragmentContainer=null;
|
||||
searchFragment=null;
|
||||
dismissingSearchFragment=false;
|
||||
}).start();
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
E.post(new FinishListCreationFragmentEvent(accountID, followList.id));
|
||||
Nav.finish(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onBackPressed(){
|
||||
if(searchFragment!=null){
|
||||
dismissSearchFragment();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAccountInList(AccountViewModel account){
|
||||
return accountIDsInList.contains(account.account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addAccountToList(AccountViewModel account, Runnable onDone){
|
||||
new AddAccountsToList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.add(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
int pos=data.size();
|
||||
data.add(account);
|
||||
list.getAdapter().notifyItemInserted(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeAccountAccountFromList(AccountViewModel account, Runnable onDone){
|
||||
new RemoveAccountsFromList(followList.id, Set.of(account.account.id))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
accountIDsInList.remove(account.account.id);
|
||||
if(onDone!=null)
|
||||
onDone.run();
|
||||
int i=0;
|
||||
for(AccountViewModel acc:data){
|
||||
if(acc.account.id.equals(account.account.id)){
|
||||
list.getAdapter().notifyItemChanged(i);
|
||||
return;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
holder.setOnLongClickListener(vh->false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(isAccountInList(account)){
|
||||
removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base){
|
||||
// TODO this
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.CreateList;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.FinishListCreationFragmentEvent;
|
||||
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class CreateListFragment extends BaseEditListFragment{
|
||||
private Button nextButton;
|
||||
private View buttonBar;
|
||||
private FollowList followList;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.create_list);
|
||||
setSubtitle(getString(R.string.step_x_of_y, 1, 2));
|
||||
setLayout(R.layout.fragment_login);
|
||||
if(savedInstanceState!=null)
|
||||
followList=Parcels.unwrap(savedInstanceState.getParcelable("list"));
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int getNavigationIconDrawableResource(){
|
||||
return R.drawable.ic_baseline_arrow_drop_down_18;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsCustomNavigationIcon(){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
nextButton=view.findViewById(R.id.btn_next);
|
||||
nextButton.setOnClickListener(this::onNextClick);
|
||||
nextButton.setText(R.string.create);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<View> getViewsForElevationEffect(){
|
||||
return List.of(getToolbar(), buttonBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable("list", Parcels.wrap(followList));
|
||||
}
|
||||
|
||||
private void onNextClick(View v){
|
||||
String title=titleEdit.getText().toString().trim();
|
||||
if(TextUtils.isEmpty(title)){
|
||||
titleEditLayout.setErrorState(getString(R.string.required_form_field_blank));
|
||||
return;
|
||||
}
|
||||
if(followList==null){
|
||||
new CreateList(title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(false);
|
||||
E.post(new ListCreatedEvent(accountID, result));
|
||||
AccountSessionManager.get(accountID).getCacheController().addList(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else if(!title.equals(followList.title) || getSelectedRepliesPolicy()!=followList.repliesPolicy || exclusiveItem.checked!=followList.exclusive){
|
||||
new UpdateList(followList.id, title, getSelectedRepliesPolicy(), exclusiveItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
followList=result;
|
||||
proceed(true);
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}else{
|
||||
proceed(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void proceed(boolean needLoadMembers){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(followList));
|
||||
args.putBoolean("needLoadMembers", needLoadMembers);
|
||||
Nav.go(getActivity(), CreateListAddMembersFragment.class, args);
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onFinishListCreationFragment(FinishListCreationFragmentEvent ev){
|
||||
if(ev.accountID.equals(accountID) && followList!=null && ev.listID.equals(followList.id)){
|
||||
Nav.finish(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ public class CustomLocalTimelineFragment extends PinnableStatusListFragment impl
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, count, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetPublicTimeline(true, false, refreshing ? null : maxID, null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.UpdateList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class EditListFragment extends BaseEditListFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.edit_list);
|
||||
loadMembers();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
menu.add(R.string.delete_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.delete_list)
|
||||
.setMessage(getString(R.string.delete_list_confirm, followList.title))
|
||||
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList())
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
String newTitle=titleEdit.getText().toString();
|
||||
FollowList.RepliesPolicy newRepliesPolicy=getSelectedRepliesPolicy();
|
||||
boolean newExclusive=exclusiveItem.checked;
|
||||
if(!newTitle.equals(followList.title) || newRepliesPolicy!=followList.repliesPolicy || newExclusive!=followList.exclusive){
|
||||
new UpdateList(followList.id, newTitle, newRepliesPolicy, newExclusive)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(FollowList result){
|
||||
AccountSessionManager.get(accountID).getCacheController().updateList(result);
|
||||
E.post(new ListUpdatedEvent(accountID, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
// TODO handle errors somehow
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,17 +41,14 @@ 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.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.CustomLocalTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TextInputFrameLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
@@ -74,7 +71,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines=new ArrayList<>();
|
||||
private final List<FollowList> followLists =new ArrayList<>();
|
||||
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
private final List<CustomLocalTimeline> localTimelines = new ArrayList<>();
|
||||
@@ -94,8 +91,8 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
|
||||
new GetLists().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result){
|
||||
listTimelines.addAll(result);
|
||||
public void onSuccess(List<FollowList> result){
|
||||
followLists.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@@ -224,7 +221,7 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
||||
followLists.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));
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@ import org.joinmastodon.android.model.FilterAction;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.FilterKeyword;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
@@ -63,6 +65,7 @@ public class HashtagTimelineFragment extends PinnableStatusListFragment{
|
||||
private MenuItem followMenuItem, pinMenuItem, muteMenuItem;
|
||||
private boolean followRequestRunning;
|
||||
private boolean toolbarContentVisible;
|
||||
private String maxID;
|
||||
|
||||
private List<String> any;
|
||||
private List<String> all;
|
||||
|
||||
@@ -5,8 +5,6 @@ import android.app.Fragment;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.graphics.drawable.RippleDrawable;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Outline;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
@@ -37,7 +35,7 @@ import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestions
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.PaginatedResponse;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.TabBar;
|
||||
|
||||
@@ -53,7 +53,7 @@ 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;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.SimpleViewHolder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
@@ -95,7 +95,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
private ImageView collapsedChevron;
|
||||
private TextView timelineTitle;
|
||||
private PopupMenu switcherPopup;
|
||||
private final Map<Integer, ListTimeline> listItems = new HashMap<>();
|
||||
private final Map<Integer, FollowList> listItems = new HashMap<>();
|
||||
private final Map<Integer, Hashtag> hashtagsItems = new HashMap<>();
|
||||
private List<TimelineDefinition> timelinesList;
|
||||
private int count;
|
||||
@@ -270,7 +270,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
public void onSuccess(List<FollowList> lists) {
|
||||
updateList(lists, listItems);
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
int id = item.getItemId();
|
||||
ListTimeline list;
|
||||
FollowList list;
|
||||
Hashtag hashtag;
|
||||
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
@@ -701,13 +701,13 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
|
||||
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
handleListEvent(listItems, l -> l.id.equals(event.id), false, null);
|
||||
handleListEvent(listItems, l -> l.id.equals(event.listID), false, null);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
handleListEvent(listItems, l -> l.id.equals(event.id), true, () -> {
|
||||
ListTimeline list = new ListTimeline();
|
||||
FollowList list = new FollowList();
|
||||
list.id = event.id;
|
||||
list.title = event.title;
|
||||
list.repliesPolicy = event.repliesPolicy;
|
||||
|
||||
@@ -0,0 +1,324 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||
import org.joinmastodon.android.api.requests.lists.AddAccountsToList;
|
||||
import org.joinmastodon.android.api.requests.lists.GetListAccounts;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.AccountAddedToListEvent;
|
||||
import org.joinmastodon.android.events.AccountRemovedFromListEvent;
|
||||
import org.joinmastodon.android.fragments.account_list.AddListMembersFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.PaginatedAccountListFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.ActionModeHelper;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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 ListMembersFragment extends PaginatedAccountListFragment{
|
||||
private static final int ADD_MEMBER_RESULT=600;
|
||||
|
||||
private ImageButton fab;
|
||||
private FollowList followList;
|
||||
private boolean inSelectionMode;
|
||||
private Set<String> selectedAccounts=new HashSet<>();
|
||||
private ActionMode actionMode;
|
||||
private MenuItem deleteItem;
|
||||
|
||||
public ListMembersFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
followList=Parcels.unwrap(getArguments().getParcelable("list"));
|
||||
setTitle(R.string.list_members);
|
||||
setHasOptionsMenu(true);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderPaginationRequest<Account> onCreateRequest(String maxID, int count){
|
||||
return new GetListAccounts(followList.id, maxID, count);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected MastodonAPIRequest loadRemoteInfo(){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getCurrentInfo(){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRemoteDomain(){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||
holder.setOnClickListener(this::onItemClick);
|
||||
holder.setOnLongClickListener(this::onItemLongClick);
|
||||
holder.getContextMenu().getMenu().add(0, R.id.remove_from_list, 0, R.string.remove_from_list);
|
||||
holder.setOnCustomMenuItemSelectedListener(item->onItemMenuItemSelected(holder, item));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
super.onBindViewHolder(holder);
|
||||
holder.setStyle(inSelectionMode ? AccountViewHolder.AccessoryType.CHECKBOX : AccountViewHolder.AccessoryType.MENU, false);
|
||||
if(inSelectionMode){
|
||||
holder.setChecked(selectedAccounts.contains(holder.getItem().account.id));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean wantsLightStatusBar(){
|
||||
if(actionMode!=null)
|
||||
return UiUtils.isDarkTheme();
|
||||
return super.wantsLightStatusBar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.selectable_list, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.select){
|
||||
enterSelectionMode();
|
||||
}else if(id==R.id.select_all){
|
||||
for(AccountViewModel a:(ArrayList<AccountViewModel>)data){
|
||||
selectedAccounts.add(a.account.id);
|
||||
}
|
||||
enterSelectionMode();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
|
||||
fab.setContentDescription(getString(R.string.add_list_member));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(insets);
|
||||
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFragmentResult(int reqCode, boolean success, Bundle result){
|
||||
if(reqCode==ADD_MEMBER_RESULT && success){
|
||||
Account acc=Objects.requireNonNull(Parcels.unwrap(result.getParcelable("selectedAccount")));
|
||||
addAccounts(List.of(acc));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountRemovedFromList(AccountRemovedFromListEvent ev){
|
||||
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||
removeAccountRows(Set.of(ev.targetAccountID));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onAccountAddedToList(AccountAddedToListEvent ev){
|
||||
if(ev.accountID.equals(accountID) && ev.listID.equals(followList.id)){
|
||||
data.add(new AccountViewModel(ev.account, accountID));
|
||||
list.getAdapter().notifyItemInserted(data.size()-1);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.goForResult(getActivity(), AddListMembersFragment.class, args, ADD_MEMBER_RESULT, this);
|
||||
}
|
||||
|
||||
private void onItemClick(AccountViewHolder holder){
|
||||
if(inSelectionMode){
|
||||
String id=holder.getItem().account.id;
|
||||
if(selectedAccounts.contains(id)){
|
||||
selectedAccounts.remove(id);
|
||||
holder.setChecked(false);
|
||||
}else{
|
||||
selectedAccounts.add(id);
|
||||
holder.setChecked(true);
|
||||
}
|
||||
updateActionModeTitle();
|
||||
deleteItem.setEnabled(!selectedAccounts.isEmpty());
|
||||
return;
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("profileAccount", Parcels.wrap(holder.getItem().account));
|
||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||
}
|
||||
|
||||
private boolean onItemLongClick(AccountViewHolder holder){
|
||||
if(inSelectionMode)
|
||||
return false;
|
||||
selectedAccounts.add(holder.getItem().account.id);
|
||||
enterSelectionMode();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onItemMenuItemSelected(AccountViewHolder holder, MenuItem item){
|
||||
int id=item.getItemId();
|
||||
if(id==R.id.remove_from_list){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.confirm_remove_list_member)
|
||||
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(Set.of(holder.getItem().account.id)))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateItemsForSelectionModeTransition(){
|
||||
list.getAdapter().notifyItemRangeChanged(0, data.size());
|
||||
}
|
||||
|
||||
private void enterSelectionMode(){
|
||||
inSelectionMode=true;
|
||||
updateItemsForSelectionModeTransition();
|
||||
V.setVisibilityAnimated(fab, View.INVISIBLE);
|
||||
actionMode=ActionModeHelper.startActionMode(this, ()->elevationOnScrollListener.getCurrentStatusBarColor(), new ActionMode.Callback(){
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu){
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu){
|
||||
mode.getMenuInflater().inflate(R.menu.settings_filter_words_action_mode, menu);
|
||||
deleteItem=menu.findItem(R.id.delete);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.confirm_remove_list_members)
|
||||
.setPositiveButton(R.string.remove, (dlg, which)->removeAccounts(new HashSet<>(selectedAccounts)))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode){
|
||||
actionMode=null;
|
||||
inSelectionMode=false;
|
||||
selectedAccounts.clear();
|
||||
updateItemsForSelectionModeTransition();
|
||||
V.setVisibilityAnimated(fab, View.VISIBLE);
|
||||
}
|
||||
});
|
||||
updateActionModeTitle();
|
||||
}
|
||||
|
||||
private void updateActionModeTitle(){
|
||||
actionMode.setTitle(getResources().getQuantityString(R.plurals.x_items_selected, selectedAccounts.size(), selectedAccounts.size()));
|
||||
}
|
||||
|
||||
private void removeAccounts(Set<String> ids){
|
||||
new RemoveAccountsFromList(followList.id, ids)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
if(inSelectionMode)
|
||||
actionMode.finish();
|
||||
removeAccountRows(ids);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void addAccounts(Collection<Account> accounts){
|
||||
new AddAccountsToList(followList.id, accounts.stream().map(a->a.id).collect(Collectors.toSet()))
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
for(Account acc:accounts){
|
||||
data.add(new AccountViewModel(acc, accountID));
|
||||
}
|
||||
list.getAdapter().notifyItemRangeInserted(data.size()-accounts.size(), accounts.size());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
private void removeAccountRows(Set<String> ids){
|
||||
for(int i=data.size()-1;i>=0;i--){
|
||||
if(ids.contains(((ArrayList<AccountViewModel>)data).get(i).account.id)){
|
||||
data.remove(i);
|
||||
list.getAdapter().notifyItemRemoved(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.TimelineDefinition;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
@@ -39,7 +39,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
private String listID;
|
||||
private String listTitle;
|
||||
@Nullable
|
||||
private ListTimeline.RepliesPolicy repliesPolicy;
|
||||
private FollowList.RepliesPolicy repliesPolicy;
|
||||
private boolean exclusive;
|
||||
|
||||
@Override
|
||||
@@ -54,19 +54,19 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
listID = args.getString("listID");
|
||||
listTitle = args.getString("listTitle");
|
||||
exclusive = args.getBoolean("listIsExclusive");
|
||||
repliesPolicy = ListTimeline.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
repliesPolicy = FollowList.RepliesPolicy.values()[args.getInt("repliesPolicy", 0)];
|
||||
|
||||
setTitle(listTitle);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
new GetList(listID).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline listTimeline) {
|
||||
public void onSuccess(FollowList followList) {
|
||||
if(getActivity()==null) return;
|
||||
// TODO: save updated info
|
||||
if (!listTimeline.title.equals(listTitle)) setTitle(listTimeline.title);
|
||||
if (listTimeline.repliesPolicy != null && !listTimeline.repliesPolicy.equals(repliesPolicy)) {
|
||||
repliesPolicy = listTimeline.repliesPolicy;
|
||||
if (!followList.title.equals(listTitle)) setTitle(followList.title);
|
||||
if (followList.repliesPolicy != null && !followList.repliesPolicy.equals(repliesPolicy)) {
|
||||
repliesPolicy = followList.repliesPolicy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
String newTitle = editor.getTitle().trim();
|
||||
setTitle(newTitle);
|
||||
new UpdateList(listID, newTitle, editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
new UpdateList(listID, newTitle, editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
public void onSuccess(FollowList list) {
|
||||
if(getActivity()==null) return;
|
||||
setTitle(list.title);
|
||||
listTitle = list.title;
|
||||
@@ -119,7 +119,7 @@ public class ListTimelineFragment extends PinnableStatusListFragment {
|
||||
.show();
|
||||
} else if (item.getItemId() == R.id.delete) {
|
||||
UiUtils.confirmDeleteList(getActivity(), accountID, listID, listTitle, () -> {
|
||||
E.post(new ListDeletedEvent(listID));
|
||||
E.post(new ListDeletedEvent(accountID, listID));
|
||||
Nav.finish(this);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.requests.lists.GetLists;
|
||||
import org.joinmastodon.android.api.requests.lists.RemoveAccountsFromList;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedCreatedEvent;
|
||||
import org.joinmastodon.android.model.ListTimeline;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.views.ListEditor;
|
||||
@@ -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 MastodonRecyclerFragment<ListTimeline> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
public class ListsFragment extends MastodonRecyclerFragment<FollowList> implements ScrollableToTop, ProvidesAssistContent.ProvidesWebUri {
|
||||
private String accountID;
|
||||
private String profileAccountId;
|
||||
private final HashMap<String, Boolean> userInListBefore = new HashMap<>();
|
||||
@@ -97,9 +97,9 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
.setIcon(R.drawable.ic_fluent_people_add_28_regular)
|
||||
.setView(editor)
|
||||
.setPositiveButton(R.string.sk_create, (d, which) ->
|
||||
new CreateList(editor.getTitle(), editor.isExclusive(), editor.getRepliesPolicy()).setCallback(new Callback<>() {
|
||||
new CreateList(editor.getTitle(), editor.getRepliesPolicy(), editor.isExclusive()).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(ListTimeline list) {
|
||||
public void onSuccess(FollowList list) {
|
||||
data.add(0, list);
|
||||
adapter.notifyItemRangeInserted(0, 1);
|
||||
E.post(new ListUpdatedCreatedEvent(list.id, list.title, list.exclusive, list.repliesPolicy));
|
||||
@@ -120,16 +120,16 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
private void saveListMembership(String listId, boolean isMember) {
|
||||
userInList.put(listId, isMember);
|
||||
List<String> accountIdList = Collections.singletonList(profileAccountId);
|
||||
MastodonAPIRequest<Object> req = isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList);
|
||||
req.setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Object o) {}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
// MastodonAPIRequest<Object> req = (MastodonAPIRequest<Object>) (isMember ? new AddAccountsToList(listId, accountIdList) : new RemoveAccountsFromList(listId, accountIdList));
|
||||
// req.setCallback(new Callback<>() {
|
||||
// @Override
|
||||
// public void onSuccess(Object o) {}
|
||||
//
|
||||
// @Override
|
||||
// public void onError(ErrorResponse error) {
|
||||
// error.showToast(getContext());
|
||||
// }
|
||||
// }).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -139,19 +139,19 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
currentRequest=(profileAccountId != null ? new GetLists(profileAccountId) : new GetLists())
|
||||
.setCallback(new SimpleCallback<>(this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> lists) {
|
||||
public void onSuccess(List<FollowList> lists) {
|
||||
if(getActivity()==null) return;
|
||||
for (ListTimeline l : lists) userInListBefore.put(l.id, true);
|
||||
for (FollowList l : lists) userInListBefore.put(l.id, true);
|
||||
userInList.putAll(userInListBefore);
|
||||
if (profileAccountId == null || !lists.isEmpty()) onDataLoaded(lists, false);
|
||||
if (profileAccountId == null) return;
|
||||
|
||||
currentRequest=new GetLists().setCallback(new SimpleCallback<>(ListsFragment.this) {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> allLists) {
|
||||
public void onSuccess(List<FollowList> allLists) {
|
||||
if(getActivity()==null) return;
|
||||
List<ListTimeline> newLists = new ArrayList<>();
|
||||
for (ListTimeline l : allLists) {
|
||||
List<FollowList> newLists = new ArrayList<>();
|
||||
for (FollowList l : allLists) {
|
||||
if (lists.stream().noneMatch(e -> e.id.equals(l.id))) newLists.add(l);
|
||||
if (!userInListBefore.containsKey(l.id)) {
|
||||
userInListBefore.put(l.id, false);
|
||||
@@ -169,8 +169,8 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
@Subscribe
|
||||
public void onListDeletedEvent(ListDeletedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
FollowList item = data.get(i);
|
||||
if (item.id.equals(event.listID)) {
|
||||
data.remove(i);
|
||||
adapter.notifyItemRemoved(i);
|
||||
break;
|
||||
@@ -181,7 +181,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
@Subscribe
|
||||
public void onListUpdatedCreatedEvent(ListUpdatedCreatedEvent event) {
|
||||
for (int i = 0; i < data.size(); i++) {
|
||||
ListTimeline item = data.get(i);
|
||||
FollowList item = data.get(i);
|
||||
if (item.id.equals(event.id)) {
|
||||
item.title = event.title;
|
||||
item.repliesPolicy = event.repliesPolicy;
|
||||
@@ -230,7 +230,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
}
|
||||
}
|
||||
|
||||
private class ListViewHolder extends BindableViewHolder<ListTimeline> implements UsableRecyclerView.Clickable{
|
||||
private class ListViewHolder extends BindableViewHolder<FollowList> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final CheckBox listToggle;
|
||||
|
||||
@@ -241,7 +241,7 @@ public class ListsFragment extends MastodonRecyclerFragment<ListTimeline> implem
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBind(ListTimeline item) {
|
||||
public void onBind(FollowList item) {
|
||||
title.setText(item.title);
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(
|
||||
item.exclusive ? R.drawable.ic_fluent_rss_24_regular : R.drawable.ic_fluent_people_24_regular
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.tags.GetFollowedTags;
|
||||
import org.joinmastodon.android.api.requests.tags.SetTagFollowed;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.Hashtag;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ManageFollowedHashtagsFragment extends BaseSettingsFragment<Hashtag> implements ListItemWithOptionsMenu.OptionsMenuListener<Hashtag>{
|
||||
private String maxID;
|
||||
|
||||
public ManageFollowedHashtagsFragment(){
|
||||
super(100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_hashtags);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetFollowedTags(offset>0 ? maxID : null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
maxID=null;
|
||||
if(result.nextPageUri!=null)
|
||||
maxID=result.nextPageUri.getQueryParameter("max_id");
|
||||
onDataLoaded(result.stream().map(t->{
|
||||
int posts=t.getWeekPosts();
|
||||
return new ListItemWithOptionsMenu<>(t.name, getResources().getQuantityString(R.plurals.x_posts_recently, posts, posts), ManageFollowedHashtagsFragment.this,
|
||||
R.drawable.ic_fluent_tag_24_regular, ManageFollowedHashtagsFragment.this::onItemClick, t, false);
|
||||
}).collect(Collectors.toList()), maxID!=null);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<Hashtag> item, Menu menu){
|
||||
menu.clear();
|
||||
menu.add(getString(R.string.unfollow_user, "#"+item.parentObject.name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemOptionSelected(ListItemWithOptionsMenu<Hashtag> item, MenuItem menuItem){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(getString(R.string.unfollow_confirmation, "#"+item.parentObject.name))
|
||||
.setPositiveButton(R.string.unfollow, (dlg, which)->doUnfollow(item))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void onItemClick(ListItemWithOptionsMenu<Hashtag> item){
|
||||
UiUtils.openHashtagTimeline(getActivity(), accountID, item.parentObject);
|
||||
}
|
||||
|
||||
private void doUnfollow(ListItemWithOptionsMenu<Hashtag> item){
|
||||
new SetTagFollowed(item.parentObject.name, false)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Hashtag result){
|
||||
int index=data.indexOf(item);
|
||||
if(index==-1)
|
||||
return;
|
||||
data.remove(index);
|
||||
list.getAdapter().notifyItemRemoved(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.lists.DeleteList;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.ListCreatedEvent;
|
||||
import org.joinmastodon.android.events.ListDeletedEvent;
|
||||
import org.joinmastodon.android.events.ListUpdatedEvent;
|
||||
import org.joinmastodon.android.fragments.settings.BaseSettingsFragment;
|
||||
import org.joinmastodon.android.model.FollowList;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItemWithOptionsMenu;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ManageListsFragment extends BaseSettingsFragment<FollowList> implements ListItemWithOptionsMenu.OptionsMenuListener<FollowList>{
|
||||
private ImageButton fab;
|
||||
|
||||
public ManageListsFragment(){
|
||||
setListLayoutId(R.layout.recycler_fragment_with_fab);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(R.string.manage_lists);
|
||||
loadData();
|
||||
setRefreshEnabled(true);
|
||||
E.register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
E.unregister(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
Callback<List<FollowList>> callback=new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowList> result){
|
||||
onDataLoaded(result.stream().map(ManageListsFragment.this::makeItem).collect(Collectors.toList()), false);
|
||||
}
|
||||
};
|
||||
if(refreshing){
|
||||
AccountSessionManager.get(accountID)
|
||||
.getCacheController()
|
||||
.reloadLists(callback);
|
||||
}else{
|
||||
AccountSessionManager.get(accountID)
|
||||
.getCacheController()
|
||||
.getLists(callback);
|
||||
}
|
||||
}
|
||||
|
||||
private ListItem<FollowList> makeItem(FollowList l){
|
||||
return new ListItemWithOptionsMenu<>(l.title, null, ManageListsFragment.this, R.drawable.ic_list_alt_24px, ManageListsFragment.this::onListClick, l, false);
|
||||
}
|
||||
|
||||
private void onListClick(ListItemWithOptionsMenu<FollowList> item){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||
Nav.go(getActivity(), ListTimelineFragment.class, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigureListItemOptionsMenu(ListItemWithOptionsMenu<FollowList> item, Menu menu){
|
||||
menu.add(0, R.id.edit, 0, R.string.edit_list);
|
||||
menu.add(0, R.id.delete, 1, R.string.delete_list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemOptionSelected(ListItemWithOptionsMenu<FollowList> item, MenuItem menuItem){
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.edit){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("list", Parcels.wrap(item.parentObject));
|
||||
Nav.go(getActivity(), EditListFragment.class, args);
|
||||
}else if(id==R.id.delete){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.delete_list)
|
||||
.setMessage(getString(R.string.delete_list_confirm, item.parentObject.title))
|
||||
.setPositiveButton(R.string.delete, (dlg, which)->doDeleteList(item.parentObject))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
fab=view.findViewById(R.id.fab);
|
||||
fab.setImageResource(R.drawable.ic_fluent_add_24_regular);
|
||||
fab.setContentDescription(getString(R.string.create_list));
|
||||
fab.setOnClickListener(v->onFabClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
super.onApplyWindowInsets(insets);
|
||||
UiUtils.applyBottomInsetToFAB(fab, insets);
|
||||
}
|
||||
|
||||
private void doDeleteList(FollowList list){
|
||||
new DeleteList(list.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Void result){
|
||||
for(int i=0;i<data.size();i++){
|
||||
if(data.get(i).parentObject==list){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
AccountSessionManager.get(accountID).getCacheController().deleteList(list.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
Activity activity=getActivity();
|
||||
if(activity==null)
|
||||
return;
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading, true)
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListUpdated(ListUpdatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
for(ListItem<FollowList> item:data){
|
||||
if(item.parentObject.id.equals(ev.list.id)){
|
||||
item.parentObject=ev.list;
|
||||
item.title=ev.list.title;
|
||||
rebindItem(item);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListDeleted(ListDeletedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
int i=0;
|
||||
for(ListItem<FollowList> item:data){
|
||||
if(item.parentObject.id.equals(ev.listID)){
|
||||
data.remove(i);
|
||||
itemsAdapter.notifyItemRemoved(i);
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onListCreated(ListCreatedEvent ev){
|
||||
if(!ev.accountID.equals(accountID))
|
||||
return;
|
||||
ListItem<FollowList> item=makeItem(ev.list);
|
||||
data.add(item);
|
||||
((List<ListItem<FollowList>>)data).sort(Comparator.comparing(l->l.parentObject.title));
|
||||
itemsAdapter.notifyItemInserted(data.indexOf(item));
|
||||
}
|
||||
|
||||
private void onFabClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
Nav.go(getActivity(), CreateListFragment.class, args);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import android.widget.TextView;
|
||||
import android.widget.Toolbar;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
||||
import com.squareup.otto.Subscribe;
|
||||
@@ -14,6 +18,7 @@ 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.markers.SaveMarkers;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.EmojiReactionsUpdatedEvent;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
@@ -47,12 +52,13 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification> {
|
||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||
private boolean onlyMentions;
|
||||
private boolean onlyPosts;
|
||||
private String maxID;
|
||||
private boolean reloadingFromCache;
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private String unreadMarker, realUnreadMarker;
|
||||
private MenuItem markAllReadItem;
|
||||
|
||||
@Override
|
||||
protected boolean wantsComposeButton() {
|
||||
@@ -63,13 +69,8 @@ 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);
|
||||
}
|
||||
onlyMentions=AccountSessionManager.get(accountID).isNotificationsMentionsOnly();
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -81,14 +82,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onAttach(Activity activity){
|
||||
super.onAttach(activity);
|
||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
||||
onlyPosts=getArguments().getBoolean("onlyPosts", false);
|
||||
setTitle(R.string.notifications);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||
if(!onlyMentions && !onlyPosts){
|
||||
if(!onlyMentions){
|
||||
switch(n.type){
|
||||
case MENTION -> {
|
||||
if(!getLocalPrefs().notificationFilters.mention)
|
||||
@@ -164,7 +163,7 @@ 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 && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, false, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||
if(getActivity()==null)
|
||||
@@ -186,7 +185,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void onRelationshipsLoaded(){
|
||||
if(getActivity()==null)
|
||||
@@ -201,13 +200,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!dataLoading){
|
||||
if(onlyMentions){
|
||||
refresh();
|
||||
}else{
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||
if(!dataLoading && canRefreshWithoutUpsettingUser()){
|
||||
reloadingFromCache=true;
|
||||
refresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,13 +264,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean("onlyMentions", onlyMentions);
|
||||
outState.putBoolean("onlyPosts", onlyPosts);
|
||||
}
|
||||
|
||||
private Notification getNotificationByID(String id){
|
||||
for(Notification n:data){
|
||||
if(n.id.equals(id))
|
||||
@@ -380,11 +369,38 @@ 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());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
inflater.inflate(R.menu.notifications, menu);
|
||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.mark_all_read){
|
||||
markAsRead();
|
||||
resetUnreadBackground();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void markAsRead(){
|
||||
if(data.isEmpty())
|
||||
return;
|
||||
String id=data.get(0).id;
|
||||
if(ObjectIdComparator.INSTANCE.compare(id, realUnreadMarker)>0){
|
||||
new SaveMarkers(null, id).exec(accountID);
|
||||
AccountSessionManager.get(accountID).setNotificationsMarker(id, true);
|
||||
realUnreadMarker=id;
|
||||
}
|
||||
}
|
||||
|
||||
void resetUnreadBackground(){
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
nf.unreadMarker=nf.realUnreadMarker;
|
||||
@@ -396,13 +412,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
public void onRefresh(){
|
||||
super.onRefresh();
|
||||
if (getParentFragment() instanceof NotificationsFragment nf) {
|
||||
if (!onlyMentions && !onlyPosts) nf.markAsRead();
|
||||
if (!onlyMentions) nf.markAsRead();
|
||||
else AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
nf.unreadMarker=nf.realUnreadMarker=m;
|
||||
nf.updateMarkAllReadButton();
|
||||
});
|
||||
}
|
||||
resetUnreadBackground();
|
||||
AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{
|
||||
unreadMarker=realUnreadMarker=m;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMarkAllReadButton(){
|
||||
markAllReadItem.setEnabled(!data.isEmpty() && realUnreadMarker!=null && !realUnreadMarker.equals(data.get(0).id));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -420,4 +443,20 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
? "/users/" + getSession().self.username + "/interactions"
|
||||
: "/notifications").build();
|
||||
}
|
||||
|
||||
private boolean canRefreshWithoutUpsettingUser(){
|
||||
// TODO maybe reload notifications the same way we reload the home timelines, i.e. with gaps and stuff
|
||||
if(data.size()<=itemsPerPage)
|
||||
return true;
|
||||
for(int i=list.getChildCount()-1;i>=0;i--){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof StatusDisplayItem.Holder<?> itemHolder){
|
||||
String id=itemHolder.getItemID();
|
||||
for(int j=0;j<data.size();j++){
|
||||
if(data.get(j).id.equals(id))
|
||||
return j<itemsPerPage; // Can refresh the list without losing scroll position if it is within the first page
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.Fragment;
|
||||
import android.app.assist.AssistContent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.Configuration;
|
||||
@@ -21,12 +23,9 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.text.TextWatcher;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
@@ -43,8 +42,6 @@ import android.view.ViewOutlineProvider;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
@@ -72,11 +69,9 @@ import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.account_list.BlockedAccountsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.BlocksListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.MutedAccountsListFragment;
|
||||
import org.joinmastodon.android.fragments.account_list.MutesListFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsServerFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
@@ -430,7 +425,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(!usernameString.contains("@")){
|
||||
usernameString+="@"+domain;
|
||||
}
|
||||
UiUtils.copyText(username, '@'+usernameString);
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText(null, "@"+usernameString));
|
||||
UiUtils.maybeShowTextCopiedToast(getActivity());
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -743,7 +739,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
followingCount.setText(UiUtils.abbreviateNumber(account.followingCount));
|
||||
followersLabel.setText(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, account.followersCount)));
|
||||
followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, account.followingCount)));
|
||||
|
||||
|
||||
if (account.followersCount < 0) followersBtn.setVisibility(View.GONE);
|
||||
if (account.followingCount < 0) followingBtn.setVisibility(View.GONE);
|
||||
if (account.followersCount < 0 || account.followingCount < 0)
|
||||
@@ -775,7 +771,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}
|
||||
|
||||
for(AccountField field:account.fields){
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
|
||||
field.parsedValue=ssb=HtmlParser.parse(field.value, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||
field.valueEmojis=ssb.getSpans(0, ssb.length(), CustomEmojiSpan.class);
|
||||
ssb=new SpannableStringBuilder(field.name);
|
||||
HtmlParser.parseCustomEmoji(ssb, account.emojis);
|
||||
@@ -973,6 +969,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
.show();
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
}else if(id==R.id.manage_user_lists){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("targetAccount", Parcels.wrap(account));
|
||||
Nav.go(getActivity(), AddAccountToListsFragment.class, args);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1364,7 +1365,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return;
|
||||
int radius=V.dp(25);
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(TextUtils.isEmpty(account.avatar) ? getSession().getDefaultAvatarUrl() : account.avatar, ava), 0,
|
||||
new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
||||
null, accountID, new SingleImagePhotoViewerListener(avatar, avatarBorder, new int[]{radius, radius, radius, radius}, this, ()->currentPhotoViewer=null, ()->ava, null, null));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,7 +1377,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
if(drawable==null || drawable instanceof ColorDrawable)
|
||||
return;
|
||||
currentPhotoViewer=new PhotoViewer(getActivity(), createFakeAttachments(account.header, drawable), 0,
|
||||
new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||
null, accountID, new SingleImagePhotoViewerListener(cover, cover, null, this, ()->currentPhotoViewer=null, ()->drawable, ()->avatarBorder.setTranslationZ(2), ()->avatarBorder.setTranslationZ(0)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
package org.joinmastodon.android.fragments;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -11,6 +16,8 @@ import android.widget.ProgressBar;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogDefaultInstances;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.fragments.onboarding.InstanceCatalogSignupFragment;
|
||||
@@ -20,6 +27,7 @@ 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.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.ProgressBarButton;
|
||||
import org.joinmastodon.android.ui.views.SizeListenerFrameLayout;
|
||||
@@ -48,6 +56,9 @@ public class SplashFragment extends AppKitFragment{
|
||||
private ProgressBar defaultServerProgress;
|
||||
private String chosenDefaultServer=DEFAULT_SERVER;
|
||||
private boolean loadingDefaultServer, loadedDefaultServer;
|
||||
private Uri currentInviteLink;
|
||||
private ProgressDialog instanceLoadingProgress;
|
||||
private String inviteCode;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -110,19 +121,65 @@ public class SplashFragment extends AppKitFragment{
|
||||
Bundle extras=new Bundle();
|
||||
boolean isSignup=v.getId()==R.id.btn_get_started;
|
||||
extras.putBoolean("signup", isSignup);
|
||||
extras.putString("defaultServer", chosenDefaultServer);
|
||||
Nav.go(getActivity(), isSignup ? InstanceCatalogSignupFragment.class : InstanceChooserLoginFragment.class, extras);
|
||||
}
|
||||
|
||||
private void onJoinDefaultServerClick(View v){
|
||||
if(loadingDefaultServer)
|
||||
return;
|
||||
instanceLoadingProgress=new ProgressDialog(getActivity());
|
||||
instanceLoadingProgress.setCancelable(false);
|
||||
instanceLoadingProgress.setMessage(getString(R.string.loading_instance));
|
||||
instanceLoadingProgress.show();
|
||||
if(currentInviteLink!=null){
|
||||
new CheckInviteLink(currentInviteLink.getPath())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(CheckInviteLink.Response result){
|
||||
inviteCode=result.inviteCode;
|
||||
proceedWithServerDomain(currentInviteLink.getHost());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(error instanceof MastodonErrorResponse mer){
|
||||
switch(mer.httpStatus){
|
||||
case 401 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.expired_invite_link)
|
||||
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
case 404 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.invalid_invite_link)
|
||||
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, currentInviteLink.getHost(), chosenDefaultServer))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
default -> error.showToast(getActivity());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(currentInviteLink.getHost());
|
||||
return;
|
||||
}
|
||||
proceedWithServerDomain(chosenDefaultServer);
|
||||
}
|
||||
|
||||
private void proceedWithServerDomain(String domain){
|
||||
new GetInstance()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Instance result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(!result.registrations){
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
if(!result.registrations && TextUtils.isEmpty(inviteCode)){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
@@ -132,6 +189,8 @@ public class SplashFragment extends AppKitFragment{
|
||||
}
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(result));
|
||||
if(inviteCode!=null)
|
||||
args.putString("inviteCode", inviteCode);
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
|
||||
@@ -139,11 +198,12 @@ public class SplashFragment extends AppKitFragment{
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
instanceLoadingProgress.dismiss();
|
||||
instanceLoadingProgress=null;
|
||||
error.showToast(getActivity());
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
||||
.execNoAuth(chosenDefaultServer);
|
||||
.execNoAuth(domain);
|
||||
}
|
||||
|
||||
private void onLearnMoreClick(View v){
|
||||
@@ -198,7 +258,18 @@ public class SplashFragment extends AppKitFragment{
|
||||
}
|
||||
|
||||
private void loadAndChooseDefaultServer(){
|
||||
loadingDefaultServer=true;
|
||||
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
|
||||
if(clipData!=null && clipData.getItemCount()>0){
|
||||
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
|
||||
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
|
||||
currentInviteLink=Uri.parse(clipText.toString());
|
||||
defaultServerButton.setText(getString(R.string.join_server_x_with_invite, currentInviteLink.getHost()));
|
||||
}
|
||||
}else{
|
||||
loadingDefaultServer=true;
|
||||
defaultServerButton.setTextVisible(false);
|
||||
defaultServerProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
new GetCatalogDefaultInstances()
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
@@ -241,7 +312,7 @@ public class SplashFragment extends AppKitFragment{
|
||||
chosenDefaultServer=domain;
|
||||
loadingDefaultServer=false;
|
||||
loadedDefaultServer=true;
|
||||
if(defaultServerButton!=null && getActivity()!=null){
|
||||
if(defaultServerButton!=null && getActivity()!=null && currentInviteLink==null){
|
||||
defaultServerButton.setTextVisible(true);
|
||||
defaultServerProgress.setVisibility(View.GONE);
|
||||
defaultServerButton.setText(getString(R.string.join_default_server, chosenDefaultServer));
|
||||
|
||||
@@ -2,11 +2,19 @@ package org.joinmastodon.android.fragments;
|
||||
|
||||
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.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import com.squareup.otto.Subscribe;
|
||||
|
||||
import org.joinmastodon.android.E;
|
||||
@@ -23,6 +31,7 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.FilterContext;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusContext;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
|
||||
@@ -50,7 +59,13 @@ import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.imageloader.ViewImageLoader;
|
||||
import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||
@@ -58,10 +73,16 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||
private StatusContext result;
|
||||
protected boolean contextInitiallyRendered, transitionFinished, preview;
|
||||
private FrameLayout replyContainer;
|
||||
private LinearLayout replyButton;
|
||||
private ImageView replyButtonAva;
|
||||
private TextView replyButtonText;
|
||||
private int lastBottomInset;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setLayout(R.layout.fragment_thread);
|
||||
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
|
||||
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
||||
@@ -143,7 +164,7 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int deleteThisItem : deleteTheseItems) itemsToModify.remove(deleteThisItem);
|
||||
if(s.id.equals(mainStatus.id)) {
|
||||
items.add(new ExtendedFooterStatusDisplayItem(s.id, this, accountID, s.getContentStatus()));
|
||||
@@ -387,6 +408,20 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
replyContainer=view.findViewById(R.id.reply_button_wrapper);
|
||||
replyButton=replyContainer.findViewById(R.id.reply_button);
|
||||
replyButtonText=replyButton.findViewById(R.id.reply_btn_text);
|
||||
replyButtonAva=replyButton.findViewById(R.id.avatar);
|
||||
replyButton.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||
replyButton.setClipToOutline(true);
|
||||
replyButtonText.setText(getString(R.string.reply_to_user, mainStatus.account.displayName));
|
||||
replyButtonAva.setOutlineProvider(OutlineProviders.OVAL);
|
||||
replyButtonAva.setClipToOutline(true);
|
||||
replyButton.setOnClickListener(v->openReply());
|
||||
Account self=AccountSessionManager.get(accountID).self;
|
||||
if(!TextUtils.isEmpty(self.avatar)){
|
||||
ViewImageLoader.loadWithoutAnimation(replyButtonAva, getResources().getDrawable(R.drawable.image_placeholder), new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
||||
}
|
||||
UiUtils.loadCustomEmojiInTextView(toolbarTitleView);
|
||||
showContent();
|
||||
if(!loaded)
|
||||
@@ -526,4 +561,24 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
}
|
||||
super.onErrorRetryClick();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplyWindowInsets(WindowInsets insets){
|
||||
lastBottomInset=insets.getSystemWindowInsetBottom();
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(replyContainer, insets));
|
||||
}
|
||||
|
||||
private void openReply(){
|
||||
maybeShowPreReplySheet(mainStatus, ()->{
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("replyTo", Parcels.wrap(mainStatus));
|
||||
args.putBoolean("fromThreadFragment", true);
|
||||
Nav.go(getActivity(), ComposeFragment.class, args);
|
||||
});
|
||||
}
|
||||
|
||||
public int getSnackbarOffset(){
|
||||
return replyContainer.getHeight()-lastBottomInset;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.search.GetSearchResults;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.SearchResults;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.SearchViewHelper;
|
||||
@@ -14,13 +16,14 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
private String currentQuery;
|
||||
public class AccountSearchFragment extends BaseAccountListFragment{
|
||||
protected String currentQuery;
|
||||
private boolean resultDelivered;
|
||||
private SearchViewHelper searchViewHelper;
|
||||
|
||||
@@ -29,12 +32,11 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
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=new SearchViewHelper(getActivity(), getToolbarContext(), getSearchViewPlaceholder());
|
||||
searchViewHelper.setListeners(this::onQueryChanged, null);
|
||||
searchViewHelper.addDivider(contentView);
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
@@ -52,13 +54,21 @@ public class ComposeAccountSearchFragment extends BaseAccountListFragment{
|
||||
.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);
|
||||
AccountSearchFragment.this.onSuccess(result.accounts);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
protected void onSuccess(List<Account> result){
|
||||
setEmptyText(R.string.no_search_results);
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), false);
|
||||
}
|
||||
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_hint);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
@@ -0,0 +1,37 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
|
||||
public class AddListMembersFragment extends AccountSearchFragment{
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
dataLoaded();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package org.joinmastodon.android.fragments.account_list;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.Button;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountFollowing;
|
||||
import org.joinmastodon.android.api.requests.accounts.SearchAccounts;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
@SuppressLint("ValidFragment") // This shouldn't be part of any saved states anyway
|
||||
public class AddNewListMembersFragment extends AccountSearchFragment{
|
||||
private Listener listener;
|
||||
private String maxID;
|
||||
|
||||
public AddNewListMembersFragment(Listener listener){
|
||||
this.listener=listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if(TextUtils.isEmpty(currentQuery)){
|
||||
currentRequest=new GetAccountFollowing(AccountSessionManager.get(accountID).self.id, offset>0 ? maxID : null, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Account> result){
|
||||
setEmptyText("");
|
||||
onDataLoaded(result.stream().map(a->new AccountViewModel(a, accountID)).collect(Collectors.toList()), result.nextPageUri!=null);
|
||||
maxID=result.getNextPageMaxID();
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}else{
|
||||
refreshing=true;
|
||||
currentRequest=new SearchAccounts(currentQuery, 0, 0, false, true)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Account> result){
|
||||
AddNewListMembersFragment.this.onSuccess(result);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSearchViewPlaceholder(){
|
||||
return getString(R.string.search_among_people_you_follow);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.CUSTOM_BUTTON, false);
|
||||
holder.setOnLongClickListener(vh->false);
|
||||
Button button=holder.getButton();
|
||||
button.setPadding(V.dp(24), 0, V.dp(24), 0);
|
||||
button.setMinimumWidth(0);
|
||||
button.setMinWidth(0);
|
||||
button.setOnClickListener(v->{
|
||||
holder.setActionProgressVisible(true);
|
||||
holder.itemView.setHasTransientState(true);
|
||||
Runnable onDone=()->{
|
||||
holder.setActionProgressVisible(false);
|
||||
holder.itemView.setHasTransientState(false);
|
||||
onBindViewHolder(holder);
|
||||
};
|
||||
AccountViewModel account=holder.getItem();
|
||||
if(listener.isAccountInList(account)){
|
||||
listener.removeAccountAccountFromList(account, onDone);
|
||||
}else{
|
||||
listener.addAccountToList(account, onDone);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBindViewHolder(AccountViewHolder holder){
|
||||
Button button=holder.getButton();
|
||||
int textRes, styleRes;
|
||||
if(listener.isAccountInList(holder.getItem())){
|
||||
textRes=R.string.remove;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Tonal_Error;
|
||||
}else{
|
||||
textRes=R.string.add;
|
||||
styleRes=R.style.Widget_Mastodon_M3_Button_Filled;
|
||||
}
|
||||
button.setText(textRes);
|
||||
TypedArray ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.background});
|
||||
button.setBackground(ta.getDrawable(0));
|
||||
ta.recycle();
|
||||
ta=button.getContext().obtainStyledAttributes(styleRes, new int[]{android.R.attr.textColor});
|
||||
button.setTextColor(ta.getColorStateList(0));
|
||||
ta.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
// no-op
|
||||
}
|
||||
|
||||
public interface Listener{
|
||||
boolean isAccountInList(AccountViewModel account);
|
||||
void addAccountToList(AccountViewModel account, Runnable onDone);
|
||||
void removeAccountAccountFromList(AccountViewModel account, Runnable onDone);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
protected HashMap<String, Relationship> relationships=new HashMap<>();
|
||||
protected String accountID;
|
||||
protected ArrayList<APIRequest<?>> relationshipsRequests=new ArrayList<>();
|
||||
protected int itemLayoutRes=R.layout.item_account_list;
|
||||
|
||||
public BaseAccountListFragment(){
|
||||
super(40);
|
||||
@@ -74,6 +75,8 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
|
||||
protected void loadRelationships(List<AccountViewModel> accounts){
|
||||
Set<String> ids=accounts.stream().map(ai->ai.account.id).collect(Collectors.toSet());
|
||||
if(ids.isEmpty())
|
||||
return;
|
||||
GetAccountRelationships req=new GetAccountRelationships(ids);
|
||||
relationshipsRequests.add(req);
|
||||
req.setCallback(new Callback<>(){
|
||||
@@ -124,13 +127,6 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
Toolbar toolbar=getToolbar();
|
||||
if(toolbar!=null && toolbar.getNavigationIcon()!=null){
|
||||
toolbar.setNavigationContentDescription(R.string.back);
|
||||
if(hasSubtitle()){
|
||||
toolbar.setTitleTextAppearance(getActivity(), R.style.m3_title_medium);
|
||||
toolbar.setSubtitleTextAppearance(getActivity(), R.style.m3_body_medium);
|
||||
int color=UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary);
|
||||
toolbar.setTitleTextColor(color);
|
||||
toolbar.setSubtitleTextColor(color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +158,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
}
|
||||
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){}
|
||||
protected void onBindViewHolder(AccountViewHolder holder){}
|
||||
|
||||
protected class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||
public AccountsAdapter(){
|
||||
@@ -171,7 +168,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@NonNull
|
||||
@Override
|
||||
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships);
|
||||
AccountViewHolder holder=new AccountViewHolder(BaseAccountListFragment.this, parent, relationships, itemLayoutRes);
|
||||
onConfigureViewHolder(holder);
|
||||
return holder;
|
||||
}
|
||||
@@ -179,6 +176,7 @@ public abstract class BaseAccountListFragment extends MastodonRecyclerFragment<A
|
||||
@Override
|
||||
public void onBindViewHolder(AccountViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
BaseAccountListFragment.this.onBindViewHolder(holder);
|
||||
super.onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,8 @@ public abstract class PaginatedAccountListFragment<T> extends BaseAccountListFra
|
||||
}
|
||||
|
||||
remoteDisabled = !GlobalUserPreferences.allowRemoteLoading
|
||||
|| getSession().domain.equals(getRemoteDomain());
|
||||
|| getSession().domain.equals(getRemoteDomain())
|
||||
|| remoteInfoRequest == null;
|
||||
if (!remoteDisabled) {
|
||||
remoteInfoRequest = loadRemoteInfo().setCallback(new Callback<>() {
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,7 @@ import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
|
||||
public class DiscoverPostsFragment extends StatusListFragment{
|
||||
private DiscoverInfoBannerHelper bannerHelper;
|
||||
private int offset;
|
||||
private int realOffset=0;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -26,26 +26,23 @@ public class DiscoverPostsFragment extends StatusListFragment{
|
||||
bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.TRENDING_POSTS, accountID);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int o, int count){
|
||||
if(refreshing) offset=0;
|
||||
currentRequest=new GetTrendingStatuses(offset, count)
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetTrendingStatuses(offset==0 ? 0 : realOffset, count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
if(getActivity()==null) return;
|
||||
boolean empty=result.isEmpty();
|
||||
offset+=result.size();
|
||||
realOffset+=result.size();
|
||||
AccountSessionManager.get(accountID).filterStatuses(result, getFilterContext());
|
||||
onDataLoaded(result, !empty);
|
||||
onDataLoaded(result, !result.isEmpty());
|
||||
bannerHelper.onBannerBecameVisible();
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
protected RecyclerView.Adapter getAdapter(){
|
||||
MergeRecyclerAdapter adapter=new MergeRecyclerAdapter();
|
||||
bannerHelper.maybeAddBanner(list, adapter);
|
||||
adapter.addAdapter(super.getAdapter());
|
||||
|
||||
@@ -29,7 +29,7 @@ public class FederatedTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(false, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetPublicTimeline(false, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
@@ -29,7 +29,7 @@ public class LocalTimelineFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
currentRequest=new GetPublicTimeline(true, false, getMaxID(), count, getLocalPrefs().timelineReplyVisibility)
|
||||
currentRequest=new GetPublicTimeline(true, false, getMaxID(), null, count, null, getLocalPrefs().timelineReplyVisibility)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<Status> result){
|
||||
|
||||
@@ -143,7 +143,7 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
}*/
|
||||
int offset=_offset;
|
||||
currentRequest=new GetSearchResults(currentQuery, type, type==null, maxID, offset, type==null ? 0 : count)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
.setCallback(new SimpleCallback<SearchResults>(this){
|
||||
@Override
|
||||
public void onSuccess(SearchResults result){
|
||||
ArrayList<SearchResult> results=new ArrayList<>();
|
||||
@@ -164,7 +164,10 @@ public class SearchFragment extends BaseStatusListFragment<SearchResult>{
|
||||
}
|
||||
prevDisplayItems=new ArrayList<>(displayItems);
|
||||
unfilteredResults=results;
|
||||
boolean wasRefreshing=refreshing;
|
||||
onDataLoaded(filterSearchResults(results), type!=null && !results.isEmpty());
|
||||
if(wasRefreshing)
|
||||
list.scrollToPosition(0);
|
||||
}
|
||||
})
|
||||
.setTimeout(180000) // 3 minutes (searches can take a long time)
|
||||
|
||||
@@ -24,7 +24,7 @@ import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.settings.SettingsMainFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@@ -137,6 +137,9 @@ public class GoogleMadeMeAddThisFragment extends ToolbarFragment{
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
if(getArguments().containsKey("inviteCode")){
|
||||
args.putString("inviteCode", getArguments().getString("inviteCode"));
|
||||
}
|
||||
Nav.goForResult(getActivity(), SignupFragment.class, args, SIGNUP_REQUEST, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.joinmastodon.android.fragments.onboarding;
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.KeyEvent;
|
||||
@@ -38,6 +37,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
@@ -48,7 +48,6 @@ import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
@@ -61,6 +60,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
protected EditText searchEdit;
|
||||
protected Runnable searchDebouncer=this::onSearchChangedDebounced;
|
||||
protected String currentSearchQuery;
|
||||
protected String currentSearchQueryButWithCasePreserved;
|
||||
protected String loadingInstanceDomain;
|
||||
protected HashMap<String, Instance> instancesCache=new HashMap<>();
|
||||
protected View buttonBar;
|
||||
@@ -91,6 +91,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
if(event!=null && event.getAction()!=KeyEvent.ACTION_DOWN)
|
||||
return true;
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
|
||||
updateFilteredList();
|
||||
searchEdit.removeCallbacks(searchDebouncer);
|
||||
Instance instance=instancesCache.get(normalizeInstanceDomain(getCurrentSearchQuery()));
|
||||
@@ -105,6 +106,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
|
||||
protected void onSearchChangedDebounced(){
|
||||
currentSearchQuery=searchEdit.getText().toString().toLowerCase().trim();
|
||||
currentSearchQueryButWithCasePreserved=searchEdit.getText().toString().trim();
|
||||
updateFilteredList();
|
||||
loadInstanceInfo(getCurrentSearchQuery(), false);
|
||||
}
|
||||
@@ -156,6 +158,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
}
|
||||
|
||||
protected void loadInstanceInfo(String _domain, boolean isFromRedirect){
|
||||
loadInstanceInfo(_domain, isFromRedirect, null);
|
||||
}
|
||||
|
||||
protected void loadInstanceInfo(String _domain, boolean isFromRedirect, Consumer<Object> onError){
|
||||
if(TextUtils.isEmpty(_domain))
|
||||
return;
|
||||
String domain=normalizeInstanceDomain(_domain);
|
||||
@@ -180,7 +186,10 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
try{
|
||||
new URI("https://"+domain+"/api/v1/instance"); // Validate the host by trying to parse the URI
|
||||
}catch(URISyntaxException x){
|
||||
showInstanceInfoLoadError(domain, x);
|
||||
if(onError!=null)
|
||||
onError.accept(x);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, x);
|
||||
if(fakeInstance!=null){
|
||||
fakeInstance.description=getString(R.string.error);
|
||||
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||
@@ -200,10 +209,11 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
loadingInstanceDomain=null;
|
||||
result.uri=domain; // needed for instances that use domain redirection
|
||||
instancesCache.put(domain, result);
|
||||
if(instanceProgressDialog!=null || onError!=null)
|
||||
proceedWithAuthOrSignup(result);
|
||||
if(instanceProgressDialog!=null){
|
||||
instanceProgressDialog.dismiss();
|
||||
instanceProgressDialog=null;
|
||||
proceedWithAuthOrSignup(result);
|
||||
}
|
||||
if(Objects.equals(domain, getCurrentSearchQuery()) || Objects.equals(getCurrentSearchQuery(), redirects.get(domain)) || Objects.equals(getCurrentSearchQuery(), redirectsInverse.get(domain))){
|
||||
boolean found=false;
|
||||
@@ -230,11 +240,14 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
public void onError(ErrorResponse error){
|
||||
loadingInstanceRequest=null;
|
||||
if(!isFromRedirect && error instanceof MastodonErrorResponse me && me.httpStatus==404){
|
||||
fetchDomainFromHostMetaAndMaybeRetry(domain, error);
|
||||
fetchDomainFromHostMetaAndMaybeRetry(domain, error, onError);
|
||||
return;
|
||||
}
|
||||
loadingInstanceDomain=null;
|
||||
showInstanceInfoLoadError(domain, error);
|
||||
if(onError!=null)
|
||||
onError.accept(error);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, error);
|
||||
if(fakeInstance!=null && getActivity()!=null){
|
||||
fakeInstance.description=getString(R.string.error);
|
||||
if(filteredData.size()>0 && filteredData.get(0)==fakeInstance){
|
||||
@@ -283,7 +296,7 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
}
|
||||
}
|
||||
|
||||
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError){
|
||||
private void fetchDomainFromHostMetaAndMaybeRetry(String domain, Object origError, Consumer<Object> onError){
|
||||
String url="https://"+domain+"/.well-known/host-meta";
|
||||
Request req=new Request.Builder()
|
||||
.url(url)
|
||||
@@ -297,7 +310,12 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
Activity a=getActivity();
|
||||
if(a==null)
|
||||
return;
|
||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, e));
|
||||
a.runOnUiThread(()->{
|
||||
if(onError!=null)
|
||||
onError.accept(e);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, e);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -309,7 +327,13 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
return;
|
||||
try(response){
|
||||
if(!response.isSuccessful()){
|
||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, response.code()+" "+response.message()));
|
||||
a.runOnUiThread(()->{
|
||||
String err=response.code()+" "+response.message();
|
||||
if(onError!=null)
|
||||
onError.accept(err);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, err);
|
||||
});
|
||||
return;
|
||||
}
|
||||
InputSource source=new InputSource(response.body().charStream());
|
||||
@@ -328,9 +352,19 @@ abstract class InstanceCatalogFragment extends MastodonRecyclerFragment<CatalogI
|
||||
}
|
||||
}
|
||||
}
|
||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, origError));
|
||||
a.runOnUiThread(()->{
|
||||
if(onError!=null)
|
||||
onError.accept(origError);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, origError);
|
||||
});
|
||||
}catch(Exception x){
|
||||
a.runOnUiThread(()->showInstanceInfoLoadError(domain, x));
|
||||
a.runOnUiThread(()->{
|
||||
if(onError!=null)
|
||||
onError.accept(x);
|
||||
else
|
||||
showInstanceInfoLoadError(domain, x);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
@@ -12,6 +17,8 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
@@ -19,9 +26,12 @@ import android.widget.PopupMenu;
|
||||
import android.widget.RadioButton;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.MastodonErrorResponse;
|
||||
import org.joinmastodon.android.api.requests.accounts.CheckInviteLink;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogCategories;
|
||||
import org.joinmastodon.android.api.requests.catalog.GetCatalogInstances;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
@@ -29,6 +39,8 @@ import org.joinmastodon.android.model.catalog.CatalogCategory;
|
||||
import org.joinmastodon.android.model.catalog.CatalogInstance;
|
||||
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.SimpleTextWatcher;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.views.FilterChipView;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
@@ -40,7 +52,9 @@ import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -77,6 +91,9 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
private CatalogInstance.Region chosenRegion;
|
||||
private CategoryChoice categoryChoice=CategoryChoice.GENERAL;
|
||||
|
||||
private String inviteCode, inviteCodeHost;
|
||||
private AlertDialog currentInviteLinkAlert;
|
||||
|
||||
public InstanceCatalogSignupFragment(){
|
||||
super(R.layout.fragment_onboarding_common, 10);
|
||||
}
|
||||
@@ -317,7 +334,7 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
focusThing=view.findViewById(R.id.focus_thing);
|
||||
focusThing.requestFocus();
|
||||
|
||||
view.findViewById(R.id.btn_random_instance).setOnClickListener(this::onPickRandomInstanceClick);
|
||||
view.findViewById(R.id.btn_use_invite).setOnClickListener(this::onUseInviteClick);
|
||||
nextButton.setEnabled(chosenInstance!=null);
|
||||
}
|
||||
|
||||
@@ -351,91 +368,191 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
|
||||
@Override
|
||||
protected void proceedWithAuthOrSignup(Instance instance){
|
||||
if(currentInviteLinkAlert!=null){
|
||||
currentInviteLinkAlert.dismiss();
|
||||
}else if(!TextUtils.isEmpty(currentSearchQuery) && HtmlParser.INVITE_LINK_PATTERN.matcher(currentSearchQueryButWithCasePreserved).find()){
|
||||
if(TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost)){
|
||||
Uri inviteLink=Uri.parse(currentSearchQueryButWithCasePreserved);
|
||||
new CheckInviteLink(inviteLink.getPath())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(CheckInviteLink.Response result){
|
||||
inviteCodeHost=inviteLink.getHost();
|
||||
inviteCode=result.inviteCode;
|
||||
proceedWithAuthOrSignup(instance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
if(error instanceof MastodonErrorResponse mer){
|
||||
switch(mer.httpStatus){
|
||||
case 401 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.expired_invite_link)
|
||||
.setMessage(getString(R.string.expired_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
case 404 -> new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.invalid_invite_link)
|
||||
.setMessage(getString(R.string.invalid_clipboard_invite_link_alert, inviteLink.getHost(), getArguments().getString("defaultServer")))
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
default -> error.showToast(getActivity());
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.wrapProgress(getActivity(), R.string.loading_instance, true)
|
||||
.execNoAuth(inviteLink.getHost());
|
||||
return;
|
||||
}
|
||||
}
|
||||
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
if(!instance.registrations){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.instance_signup_closed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
if(!instance.registrations && (TextUtils.isEmpty(inviteCode) || !Objects.equals(instance.uri, inviteCodeHost))){
|
||||
if(instance.invitesEnabled){
|
||||
showInviteLinkAlert(instance.uri);
|
||||
}else{
|
||||
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(instance));
|
||||
if(!TextUtils.isEmpty(inviteCode) && Objects.equals(instance.uri, inviteCodeHost))
|
||||
args.putString("inviteCode", inviteCode);
|
||||
Nav.go(getActivity(), InstanceRulesFragment.class, args);
|
||||
}
|
||||
|
||||
private void onPickRandomInstanceClick(View v){
|
||||
String lang=Locale.getDefault().getLanguage();
|
||||
List<CatalogInstance> instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general"))) && (lang.equals(ci.language) || (ci.languages!=null && ci.languages.contains(lang)))).collect(Collectors.toList());
|
||||
if(instances.isEmpty()){
|
||||
instances=data.stream().filter(ci->!ci.approvalRequired && ("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
||||
}
|
||||
if(instances.isEmpty()){
|
||||
instances=data.stream().filter(ci->("general".equals(ci.category) || (ci.categories!=null && ci.categories.contains("general")))).collect(Collectors.toList());
|
||||
}
|
||||
if(instances.isEmpty()){
|
||||
return;
|
||||
}
|
||||
chosenInstance=instances.get(new Random().nextInt(instances.size()));
|
||||
onNextClick(v);
|
||||
private void onUseInviteClick(View v){
|
||||
showInviteLinkAlert(null);
|
||||
}
|
||||
|
||||
// private String getEmojiForCategory(String category){
|
||||
// return switch(category){
|
||||
// case "all" -> "💬";
|
||||
// case "academia" -> "📚";
|
||||
// case "activism" -> "✊";
|
||||
// case "food" -> "🍕";
|
||||
// case "furry" -> "🦁";
|
||||
// case "games" -> "🕹";
|
||||
// case "general" -> "🐘";
|
||||
// case "journalism" -> "📰";
|
||||
// case "lgbt" -> "🏳️🌈";
|
||||
// case "regional" -> "📍";
|
||||
// case "art" -> "🎨";
|
||||
// case "music" -> "🎼";
|
||||
// case "tech" -> "📱";
|
||||
// default -> "❓";
|
||||
// };
|
||||
// }
|
||||
private void showInviteLinkAlert(String domain){
|
||||
AlertDialog alert=new M3AlertDialogBuilder(getActivity())
|
||||
.setView(R.layout.alert_invite_link)
|
||||
.setPositiveButton(R.string.next, null)
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.create();
|
||||
|
||||
private int getEmojiForCategory(String category){
|
||||
return switch(category){
|
||||
case "all" -> R.drawable.ic_category_all;
|
||||
case "academia" -> R.drawable.ic_category_academia;
|
||||
case "activism" -> R.drawable.ic_category_activism;
|
||||
case "food" -> R.drawable.ic_category_food;
|
||||
case "furry" -> R.drawable.ic_category_furry;
|
||||
case "games" -> R.drawable.ic_category_games;
|
||||
case "general" -> R.drawable.ic_category_general;
|
||||
case "journalism" -> R.drawable.ic_category_journalism;
|
||||
case "lgbt" -> R.drawable.ic_category_lgbt;
|
||||
case "regional" -> R.drawable.ic_category_regional;
|
||||
case "art" -> R.drawable.ic_category_art;
|
||||
case "music" -> R.drawable.ic_category_music;
|
||||
case "tech" -> R.drawable.ic_category_tech;
|
||||
default -> R.drawable.ic_category_unknown;
|
||||
};
|
||||
}
|
||||
Button next=alert.getButton(AlertDialog.BUTTON_POSITIVE);
|
||||
EditText edit=alert.findViewById(R.id.edit);
|
||||
TextView supportingText=alert.findViewById(R.id.supporting_text);
|
||||
TextView label=alert.findViewById(R.id.label);
|
||||
TextView subtitle=alert.findViewById(R.id.subtitle);
|
||||
ImageButton clear=alert.findViewById(R.id.clear);
|
||||
clear.setVisibility(View.GONE);
|
||||
|
||||
private int getTitleForCategory(String category){
|
||||
return switch(category){
|
||||
case "all" -> R.string.category_all;
|
||||
case "academia" -> R.string.category_academia;
|
||||
case "activism" -> R.string.category_activism;
|
||||
case "food" -> R.string.category_food;
|
||||
case "furry" -> R.string.category_furry;
|
||||
case "games" -> R.string.category_games;
|
||||
case "general" -> R.string.category_general;
|
||||
case "journalism" -> R.string.category_journalism;
|
||||
case "lgbt" -> R.string.category_lgbt;
|
||||
case "regional" -> R.string.category_regional;
|
||||
case "art" -> R.string.category_art;
|
||||
case "music" -> R.string.category_music;
|
||||
case "tech" -> R.string.category_tech;
|
||||
default -> 0;
|
||||
if(TextUtils.isEmpty(domain)){
|
||||
subtitle.setVisibility(View.GONE);
|
||||
}else{
|
||||
subtitle.setText(getString(R.string.need_invite_to_join_server, domain));
|
||||
}
|
||||
|
||||
Consumer<String> errorSetter=err->{
|
||||
supportingText.setText(err);
|
||||
int errorColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3Error);
|
||||
supportingText.setTextColor(errorColor);
|
||||
label.setTextColor(errorColor);
|
||||
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field_error);
|
||||
};
|
||||
|
||||
next.setOnClickListener(_v->{
|
||||
Uri inviteLink=Uri.parse(edit.getText().toString());
|
||||
if(TextUtils.isEmpty(inviteLink.getHost()) || TextUtils.isEmpty(inviteLink.getPath())){
|
||||
errorSetter.accept(getString(R.string.this_invite_is_invalid));
|
||||
return;
|
||||
}
|
||||
UiUtils.showProgressForAlertButton(next, true);
|
||||
new CheckInviteLink(inviteLink.getPath())
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(CheckInviteLink.Response result){
|
||||
if(getActivity()==null || !alert.isShowing())
|
||||
return;
|
||||
|
||||
String host=inviteLink.getHost();
|
||||
inviteCode=result.inviteCode;
|
||||
inviteCodeHost=host;
|
||||
|
||||
Instance instance=instancesCache.get(normalizeInstanceDomain(host));
|
||||
if(instance==null){
|
||||
loadInstanceInfo(host, false, err->{
|
||||
String errorStr;
|
||||
if(err instanceof String str){
|
||||
errorStr=str;
|
||||
}else if(err instanceof Throwable x){
|
||||
errorStr=x.getMessage();
|
||||
}else if(err instanceof MastodonErrorResponse mer){
|
||||
errorStr=mer.error;
|
||||
}else{
|
||||
errorStr=getString(R.string.error);
|
||||
}
|
||||
errorSetter.accept(errorStr);
|
||||
UiUtils.showProgressForAlertButton(next, false);
|
||||
});
|
||||
}else{
|
||||
proceedWithAuthOrSignup(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null || !alert.isShowing())
|
||||
return;
|
||||
UiUtils.showProgressForAlertButton(next, false);
|
||||
if(error instanceof MastodonErrorResponse mer){
|
||||
errorSetter.accept(switch(mer.httpStatus){
|
||||
case 404 -> getString(R.string.this_invite_is_invalid);
|
||||
case 401 -> getString(R.string.this_invite_has_expired);
|
||||
default -> mer.error;
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.execNoAuth(inviteLink.getHost());
|
||||
});
|
||||
next.setEnabled(false);
|
||||
edit.addTextChangedListener(new SimpleTextWatcher(e->{
|
||||
boolean wasEmpty=!next.isEnabled();
|
||||
next.setEnabled(e.length()>0);
|
||||
if(supportingText.length()>0){
|
||||
supportingText.setText("");
|
||||
int regularColor=UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurfaceVariant);
|
||||
supportingText.setTextColor(regularColor);
|
||||
label.setTextColor(regularColor);
|
||||
edit.setBackgroundResource(R.drawable.bg_m3_filled_text_field);
|
||||
}
|
||||
if(wasEmpty!=(e.length()==0)){
|
||||
int padEnd;
|
||||
if(e.length()==0){
|
||||
clear.setVisibility(View.GONE);
|
||||
padEnd=V.dp(16);
|
||||
}else{
|
||||
clear.setVisibility(View.VISIBLE);
|
||||
padEnd=V.dp(48);
|
||||
}
|
||||
edit.setPaddingRelative(edit.getPaddingStart(), edit.getPaddingTop(), padEnd, edit.getPaddingBottom());
|
||||
}
|
||||
}));
|
||||
clear.setOnClickListener(_v->edit.setText(""));
|
||||
|
||||
ClipData clipData=getActivity().getSystemService(ClipboardManager.class).getPrimaryClip();
|
||||
if(clipData!=null && clipData.getItemCount()>0){
|
||||
CharSequence clipText=clipData.getItemAt(0).coerceToText(getActivity());
|
||||
if(HtmlParser.INVITE_LINK_PATTERN.matcher(clipText).find()){
|
||||
edit.setText(clipText);
|
||||
supportingText.setText(R.string.invite_link_pasted);
|
||||
}
|
||||
}
|
||||
|
||||
currentInviteLinkAlert=alert;
|
||||
alert.setOnDismissListener(dialog->currentInviteLinkAlert=null);
|
||||
alert.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -444,8 +561,14 @@ public class InstanceCatalogSignupFragment extends InstanceCatalogFragment imple
|
||||
filteredData.clear();
|
||||
if(searchQueryMode){
|
||||
if(!TextUtils.isEmpty(currentSearchQuery)){
|
||||
String actualQuery;
|
||||
if(currentSearchQuery.startsWith("https:")){
|
||||
actualQuery=Uri.parse(currentSearchQuery).getHost();
|
||||
}else{
|
||||
actualQuery=currentSearchQuery;
|
||||
}
|
||||
for(CatalogInstance instance:data){
|
||||
if(instance.domain.contains(currentSearchQuery)){
|
||||
if(instance.domain.contains(actualQuery)){
|
||||
filteredData.add(instance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,9 @@ public class InstanceRulesFragment extends ToolbarFragment implements ProvidesAs
|
||||
protected void onButtonClick(){
|
||||
Bundle args=new Bundle();
|
||||
args.putParcelable("instance", Parcels.wrap(instance));
|
||||
if(getArguments().containsKey("inviteCode")){
|
||||
args.putString("inviteCode", getArguments().getString("inviteCode"));
|
||||
}
|
||||
Nav.goForResult(getActivity(), GoogleMadeMeAddThisFragment.class, args, RULES_REQUEST, this);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetFollowSuggestions;
|
||||
@@ -13,35 +14,38 @@ import org.joinmastodon.android.fragments.account_list.BaseAccountListFragment;
|
||||
import org.joinmastodon.android.model.FollowSuggestion;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.AccountViewHolder;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
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.api.SimpleCallback;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment{
|
||||
private String accountID;
|
||||
private View buttonBar;
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private int numRunningFollowRequests=0;
|
||||
|
||||
public OnboardingFollowSuggestionsFragment(){
|
||||
super(R.layout.fragment_onboarding_follow_suggestions, 40);
|
||||
itemLayoutRes=R.layout.item_account_list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setRetainInstance(true);
|
||||
setTitle(R.string.popular_on_mastodon);
|
||||
setTitle(R.string.onboarding_recommendations_title);
|
||||
accountID=getArguments().getString("account");
|
||||
loadData();
|
||||
}
|
||||
@@ -50,7 +54,6 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
buttonBar=view.findViewById(R.id.button_bar);
|
||||
list.addOnScrollListener(onScrollListener=new ElevationOnScrollListener((FragmentRootLinearLayout) view, buttonBar, getToolbar()));
|
||||
|
||||
view.findViewById(R.id.btn_next).setOnClickListener(UiUtils.rateLimitedClickListener(this::onFollowAllClick));
|
||||
// view.findViewById(R.id.btn_skip).setOnClickListener(UiUtils.rateLimitedClickListener(v->proceed()));
|
||||
@@ -59,9 +62,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
@Override
|
||||
protected void onUpdateToolbar(){
|
||||
super.onUpdateToolbar();
|
||||
if(onScrollListener!=null){
|
||||
onScrollListener.setViews(buttonBar, getToolbar());
|
||||
}
|
||||
getToolbar().setContentInsetsRelative(V.dp(56), 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -70,7 +71,7 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(List<FollowSuggestion> result){
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID)).collect(Collectors.toList()), false);
|
||||
onDataLoaded(result.stream().map(fs->new AccountViewModel(fs.account, accountID).stripLinksFromBio()).collect(Collectors.toList()), false);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
@@ -81,6 +82,20 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
// Unused in Moshidon
|
||||
// TextView introText=new TextView(getActivity());
|
||||
// introText.setTextAppearance(R.style.m3_body_large);
|
||||
// introText.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3OnSurface));
|
||||
// introText.setPaddingRelative(V.dp(56), 0, V.dp(24), V.dp(8));
|
||||
// introText.setText(R.string.onboarding_recommendations_intro);
|
||||
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||
// mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(introText));
|
||||
mergeAdapter.addAdapter(super.getAdapter());
|
||||
return mergeAdapter;
|
||||
}
|
||||
|
||||
private void onFollowAllClick(View v){
|
||||
if(!loaded || relationships.isEmpty())
|
||||
return;
|
||||
@@ -156,6 +171,12 @@ public class OnboardingFollowSuggestionsFragment extends BaseAccountListFragment
|
||||
protected void onConfigureViewHolder(AccountViewHolder holder){
|
||||
super.onConfigureViewHolder(holder);
|
||||
holder.setStyle(AccountViewHolder.AccessoryType.BUTTON, true);
|
||||
holder.avatar.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getWebUri(Uri.Builder base){
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.view.WindowInsets;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ScrollView;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
@@ -19,12 +20,17 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HomeFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||
import org.joinmastodon.android.ui.views.ReorderableLinearLayout;
|
||||
import org.joinmastodon.android.utils.ElevationOnScrollListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
@@ -35,7 +41,7 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class OnboardingProfileSetupFragment extends ToolbarFragment implements ReorderableLinearLayout.OnDragListener{
|
||||
public class OnboardingProfileSetupFragment extends ToolbarFragment{
|
||||
private Button btn;
|
||||
private View buttonBar;
|
||||
private String accountID;
|
||||
@@ -43,9 +49,9 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
private ScrollView scroller;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ImageView avaImage, coverImage;
|
||||
private Button addRow;
|
||||
private ReorderableLinearLayout profileFieldsLayout;
|
||||
private Uri avatarUri, coverUri;
|
||||
private LinearLayout scrollContent;
|
||||
private CheckableListItem<Void> discoverableItem;
|
||||
|
||||
private static final int AVATAR_RESULT=348;
|
||||
private static final int COVER_RESULT=183;
|
||||
@@ -73,8 +79,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
bioEdit=view.findViewById(R.id.bio);
|
||||
avaImage=view.findViewById(R.id.avatar);
|
||||
coverImage=view.findViewById(R.id.header);
|
||||
addRow=view.findViewById(R.id.add_row);
|
||||
profileFieldsLayout=view.findViewById(R.id.profile_fields);
|
||||
|
||||
btn=view.findViewById(R.id.btn_next);
|
||||
btn.setOnClickListener(v->onButtonClick());
|
||||
@@ -86,31 +90,20 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
Account account=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||
if(savedInstanceState==null){
|
||||
nameEdit.setText(account.displayName);
|
||||
makeFieldsRow();
|
||||
}else{
|
||||
ArrayList<String> fieldTitles=savedInstanceState.getStringArrayList("fieldTitles");
|
||||
ArrayList<String> fieldValues=savedInstanceState.getStringArrayList("fieldValues");
|
||||
for(int i=0;i<fieldTitles.size();i++){
|
||||
View row=makeFieldsRow();
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
title.setText(fieldTitles.get(i));
|
||||
content.setText(fieldValues.get(i));
|
||||
}
|
||||
if(fieldTitles.size()==4)
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
addRow.setOnClickListener(v->{
|
||||
makeFieldsRow();
|
||||
if(profileFieldsLayout.getChildCount()==4){
|
||||
addRow.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
profileFieldsLayout.setDragListener(this);
|
||||
avaImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), AVATAR_RESULT));
|
||||
coverImage.setOnClickListener(v->startActivityForResult(UiUtils.getMediaPickerIntent(new String[]{"image/*"}, 1), COVER_RESULT));
|
||||
|
||||
scrollContent=view.findViewById(R.id.scrollable_content);
|
||||
discoverableItem=new CheckableListItem<>(R.string.make_profile_discoverable, 0, CheckableListItem.Style.SWITCH_SEPARATED, true, R.drawable.ic_campaign_24px, item->showDiscoverabilityAlert());
|
||||
GenericListItemsAdapter<Void> fakeAdapter=new GenericListItemsAdapter<>(List.of(discoverableItem));
|
||||
ListItemViewHolder<?> holder=fakeAdapter.onCreateViewHolder(scrollContent, fakeAdapter.getItemViewType(0));
|
||||
fakeAdapter.bindViewHolder(holder, 0);
|
||||
holder.itemView.setBackground(UiUtils.getThemeDrawable(getActivity(), android.R.attr.selectableItemBackground));
|
||||
holder.itemView.setOnClickListener(v->holder.onClick());
|
||||
scrollContent.addView(holder.itemView);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@@ -129,17 +122,8 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
}
|
||||
|
||||
protected void onButtonClick(){
|
||||
ArrayList<AccountField> fields=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
AccountField fld=new AccountField();
|
||||
fld.name=title.getText().toString();
|
||||
fld.value=content.getText().toString();
|
||||
fields.add(fld);
|
||||
}
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, fields)
|
||||
new UpdateAccountCredentials(nameEdit.getText().toString(), bioEdit.getText().toString(), avatarUri, coverUri, null)
|
||||
.setDiscoverableIndexable(discoverableItem.checked, discoverableItem.checked)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Account result){
|
||||
@@ -163,39 +147,6 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
super.onApplyWindowInsets(UiUtils.applyBottomInsetToFixedView(buttonBar, insets));
|
||||
}
|
||||
|
||||
private View makeFieldsRow(){
|
||||
View view=LayoutInflater.from(getActivity()).inflate(R.layout.onboarding_profile_field, profileFieldsLayout, false);
|
||||
profileFieldsLayout.addView(view);
|
||||
view.findViewById(R.id.dragger_thingy).setOnLongClickListener(v->{
|
||||
profileFieldsLayout.startDragging(view);
|
||||
return true;
|
||||
});
|
||||
view.findViewById(R.id.delete).setOnClickListener(v->{
|
||||
profileFieldsLayout.removeView(view);
|
||||
if(addRow.getVisibility()==View.GONE)
|
||||
addRow.setVisibility(View.VISIBLE);
|
||||
});
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwapItems(int oldIndex, int newIndex){}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState){
|
||||
super.onSaveInstanceState(outState);
|
||||
ArrayList<String> fieldTitles=new ArrayList<>(), fieldValues=new ArrayList<>();
|
||||
for(int i=0;i<profileFieldsLayout.getChildCount();i++){
|
||||
View row=profileFieldsLayout.getChildAt(i);
|
||||
EditText title=row.findViewById(R.id.title);
|
||||
EditText content=row.findViewById(R.id.content);
|
||||
fieldTitles.add(title.getText().toString());
|
||||
fieldValues.add(content.getText().toString());
|
||||
}
|
||||
outState.putStringArrayList("fieldTitles", fieldTitles);
|
||||
outState.putStringArrayList("fieldValues", fieldValues);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(resultCode!=Activity.RESULT_OK)
|
||||
@@ -215,4 +166,12 @@ public class OnboardingProfileSetupFragment extends ToolbarFragment implements R
|
||||
img.setForeground(null);
|
||||
ViewImageLoader.load(img, null, new UrlImageLoaderRequest(uri, size, size));
|
||||
}
|
||||
|
||||
private void showDiscoverabilityAlert(){
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.discoverability)
|
||||
.setMessage(R.string.discoverability_help)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
package org.joinmastodon.android.fragments.onboarding;
|
||||
|
||||
import android.app.ProgressDialog;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.LocaleList;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -46,10 +43,13 @@ import org.jsoup.select.NodeVisitor;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -58,7 +58,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;
|
||||
import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||
|
||||
public class SignupFragment extends ToolbarFragment{
|
||||
@@ -79,6 +78,7 @@ public class SignupFragment extends ToolbarFragment{
|
||||
private ProgressDialog progressDialog;
|
||||
private HashSet<EditText> errorFields=new HashSet<>();
|
||||
private ElevationOnScrollListener onScrollListener;
|
||||
private Set<String> serverSupportedTimezones, serverSupportedLocales;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
@@ -87,6 +87,8 @@ public class SignupFragment extends ToolbarFragment{
|
||||
instance=Parcels.unwrap(getArguments().getParcelable("instance"));
|
||||
createAppAndGetToken();
|
||||
setTitle(R.string.signup_title);
|
||||
serverSupportedTimezones=Arrays.stream(getResources().getStringArray(R.array.server_supported_timezones)).collect(Collectors.toSet());
|
||||
serverSupportedLocales=Arrays.stream(getResources().getStringArray(R.array.server_supported_locales)).collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -190,7 +192,36 @@ public class SignupFragment extends ToolbarFragment{
|
||||
edit.setError(null);
|
||||
}
|
||||
errorFields.clear();
|
||||
new RegisterAccount(username, email, password.getText().toString(), getResources().getConfiguration().locale.getLanguage(), reason.getText().toString(), ZoneId.systemDefault().getId())
|
||||
String locale=null;
|
||||
String timezone=ZoneId.systemDefault().getId();
|
||||
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
LocaleList localeList=getResources().getConfiguration().getLocales();
|
||||
for(int i=0;i<localeList.size();i++){
|
||||
Locale l=localeList.get(i);
|
||||
if(serverSupportedLocales.contains(l.toLanguageTag())){
|
||||
locale=l.toLanguageTag();
|
||||
break;
|
||||
}else if(serverSupportedLocales.contains(l.getLanguage())){
|
||||
locale=l.getLanguage();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
Locale l=getResources().getConfiguration().locale;
|
||||
if(serverSupportedLocales.contains(l.toLanguageTag())){
|
||||
locale=l.toLanguageTag();
|
||||
}else if(serverSupportedLocales.contains(l.getLanguage())){
|
||||
locale=l.getLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
if(!serverSupportedTimezones.contains(timezone))
|
||||
timezone=null;
|
||||
|
||||
String inviteCode=getArguments().getString("inviteCode");
|
||||
|
||||
new RegisterAccount(username, email, password.getText().toString(), locale, reason.getText().toString(), timezone, inviteCode)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Token result){
|
||||
|
||||
@@ -12,12 +12,10 @@ 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;
|
||||
@@ -45,7 +43,7 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<?> getAdapter(){
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(data);
|
||||
return itemsAdapter=new GenericListItemsAdapter<T>(imgLoader, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -59,12 +57,13 @@ public abstract class BaseSettingsFragment<T> extends MastodonRecyclerFragment<L
|
||||
return 0;
|
||||
}
|
||||
|
||||
protected void toggleCheckableItem(CheckableListItem<T> item){
|
||||
item.toggle();
|
||||
protected void toggleCheckableItem(ListItem<?> item){
|
||||
if(item instanceof CheckableListItem<?> checkable)
|
||||
checkable.toggle();
|
||||
rebindItem(item);
|
||||
}
|
||||
|
||||
protected void rebindItem(ListItem<T> item){
|
||||
protected void rebindItem(ListItem<?> item){
|
||||
if(list==null)
|
||||
return;
|
||||
if(list.findViewHolderForAdapterPosition(indexOfItemsAdapter()+data.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
@@ -18,6 +21,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HasAccountID;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
@@ -106,6 +110,14 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
|
||||
versionInfo.setTextColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3Outline));
|
||||
versionInfo.setGravity(Gravity.CENTER);
|
||||
versionInfo.setText(getString(R.string.mo_settings_app_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE));
|
||||
versionInfo.setOnClickListener(v->{
|
||||
getActivity().getSystemService(ClipboardManager.class).setPrimaryClip(ClipData.newPlainText("", BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")"));
|
||||
if(Build.VERSION.SDK_INT<=Build.VERSION_CODES.S_V2){
|
||||
new Snackbar.Builder(getActivity())
|
||||
.setText(R.string.app_version_copied)
|
||||
.show();
|
||||
}
|
||||
});
|
||||
adapter.addAdapter(new SingleViewRecyclerAdapter(versionInfo));
|
||||
|
||||
return adapter;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.api.session.AccountActivationInfo;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -28,7 +27,8 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
new ListItem<>("Test email confirmation flow", null, this::onTestEmailConfirmClick),
|
||||
selfUpdateItem=new ListItem<>("Force self-update", null, this::onForceSelfUpdateClick),
|
||||
resetUpdateItem=new ListItem<>("Reset self-updater", null, this::onResetUpdaterClick),
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick)
|
||||
new ListItem<>("Reset search info banners", null, this::onResetDiscoverBannersClick),
|
||||
new ListItem<>("Reset pre-reply sheets", null, this::onResetPreReplySheetsClick)
|
||||
));
|
||||
if(!GithubSelfUpdater.needSelfUpdating()){
|
||||
resetUpdateItem.isEnabled=selfUpdateItem.isEnabled=false;
|
||||
@@ -65,6 +65,12 @@ public class SettingsDebugFragment extends BaseSettingsFragment<Void>{
|
||||
restartUI();
|
||||
}
|
||||
|
||||
private void onResetPreReplySheetsClick(ListItem<?> item){
|
||||
// TODO fix this
|
||||
// GlobalUserPreferences.resetPreReplySheets();
|
||||
Toast.makeText(getActivity(), "Pre-reply sheets were reset", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void restartUI(){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
@@ -18,7 +17,7 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.SelfUpdateStateChangedEvent;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.sheets.AccountSwitcherSheet;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.utils.HideableSingleViewRecyclerAdapter;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
Reference in New Issue
Block a user