More Akkoma improvements (#524)

* Only open account if domain matches
Akkoma will seemingly show results that don't match well. This checks if the domain matches before continuing

* Add "RE:" for quotes where it's missing

* Fix no hashtag history in search

* Skip not implemented discovery and select search on Pleroma

* Set proper max account fields for Pleroma

* Use Pleroma's non-standard poll limits

* Mark notifications as read properly on Pleroma

* Akkoma bubble timeline

* Respect Reply Visibility preference on all timelines

* vertically center if hashtag has no history

* only open account search result if uri equals

* add getInstance and isPleroma methods

* change timelines api, support compatibility checks

---------

Co-authored-by: sk <sk22@mailbox.org>
This commit is contained in:
Jacoco 2023-05-29 02:37:46 +02:00 committed by GitHub
parent 231f19d113
commit b5f6687925
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 318 additions and 44 deletions

View File

@ -19,7 +19,6 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.PaginatedResponse;
import org.joinmastodon.android.model.SearchResult;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.utils.StatusFilterPredicate;
@ -160,7 +159,7 @@ public class CacheController{
}
}
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain);
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.pleroma != null)
new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isPleroma())
.setCallback(new Callback<>(){
@Override
public void onSuccess(List<Notification> result){

View File

@ -0,0 +1,30 @@
package org.joinmastodon.android.api.requests.notifications;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Notification;
import java.util.List;
import okhttp3.MultipartBody;
import okhttp3.RequestBody;
public class PleromaMarkNotificationsRead extends MastodonAPIRequest<List<Notification>> {
private String maxID;
public PleromaMarkNotificationsRead(String maxID) {
super(HttpMethod.POST, "/pleroma/notifications/read", new TypeToken<>(){});
this.maxID = maxID;
}
@Override
public RequestBody getRequestBody() {
MultipartBody.Builder builder=new MultipartBody.Builder()
.setType(MultipartBody.FORM);
if(!TextUtils.isEmpty(maxID))
builder.addFormDataPart("max_id", maxID);
return builder.build();
}
}

View File

@ -0,0 +1,23 @@
package org.joinmastodon.android.api.requests.timelines;
import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.util.List;
public class GetBubbleTimeline extends MastodonAPIRequest<List<Status>> {
public GetBubbleTimeline(String maxID, int limit) {
super(HttpMethod.GET, "/timelines/bubble", new TypeToken<>(){});
if(!TextUtils.isEmpty(maxID))
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -16,5 +17,7 @@ public class GetHashtagTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -2,6 +2,7 @@ package org.joinmastodon.android.api.requests.timelines;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -18,5 +19,7 @@ public class GetListTimeline extends MastodonAPIRequest<List<Status>> {
addQueryParameter("limit", ""+limit);
if(sinceID!=null)
addQueryParameter("since_id", sinceID);
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -4,6 +4,7 @@ import android.text.TextUtils;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
@ -20,5 +21,7 @@ public class GetPublicTimeline extends MastodonAPIRequest<List<Status>>{
addQueryParameter("max_id", maxID);
if(limit>0)
addQueryParameter("limit", limit+"");
if(GlobalUserPreferences.replyVisibility != null)
addQueryParameter("reply_visibility", GlobalUserPreferences.replyVisibility);
}
}

View File

@ -7,6 +7,7 @@ import org.joinmastodon.android.api.StatusInteractionController;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Application;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Markers;
import org.joinmastodon.android.model.Preferences;
import org.joinmastodon.android.model.PushSubscription;
@ -87,4 +88,8 @@ public class AccountSession{
pushSubscriptionManager=new PushSubscriptionManager(getID());
return pushSubscriptionManager;
}
public Instance getInstance() {
return AccountSessionManager.getInstance().getInstanceInfo(domain);
}
}

View File

@ -1087,7 +1087,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
}
req.status=text;
req.localOnly=localOnly;
req.visibility=localOnly && instance.pleroma != null ? StatusPrivacy.LOCAL : statusVisibility;
req.visibility=localOnly && instance.isPleroma() ? StatusPrivacy.LOCAL : statusVisibility;
req.sensitive=sensitive;
req.language=language;
req.contentType=contentType;
@ -1743,11 +1743,24 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
pollChanged=true;
updatePublishButtonState();
}));
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0 ? instance.configuration.polls.maxCharactersPerOption : 50)});
int maxCharactersPerOption = 50;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxCharactersPerOption>0)
maxCharactersPerOption = instance.configuration.polls.maxCharactersPerOption;
else if(instance.pollLimits!=null && instance.pollLimits.maxOptionChars>0)
maxCharactersPerOption = instance.pollLimits.maxOptionChars;
option.edit.setFilters(new InputFilter[]{new InputFilter.LengthFilter(maxCharactersPerOption)});
pollOptionsView.addView(option.view);
pollOptions.add(option);
if(pollOptions.size()==(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0 ? instance.configuration.polls.maxOptions : 4))
int maxPollOptions = 4;
if(instance.configuration!=null && instance.configuration.polls!=null && instance.configuration.polls.maxOptions>0)
maxPollOptions = instance.configuration.polls.maxOptions;
else if (instance.pollLimits!=null && instance.pollLimits.maxOptions>0)
maxPollOptions = instance.pollLimits.maxOptions;
if(pollOptions.size()==maxPollOptions)
addPollOptionBtn.setVisibility(View.GONE);
return option;
}
@ -1889,7 +1902,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
Menu m=visibilityPopup.getMenu();
MenuItem localOnlyItem = visibilityPopup.getMenu().findItem(R.id.local_only);
boolean prefsSaysSupported = GlobalUserPreferences.accountsWithLocalOnlySupport.contains(accountID);
if (instance.pleroma != null) {
if (instance.isPleroma()) {
m.findItem(R.id.vis_local).setVisible(true);
} else if (localOnly || prefsSaysSupported) {
localOnlyItem.setVisible(true);

View File

@ -30,8 +30,11 @@ import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.lists.GetLists;
import org.joinmastodon.android.api.requests.tags.GetFollowedHashtags;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Hashtag;
import org.joinmastodon.android.model.HeaderPaginationList;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.ListTimeline;
import org.joinmastodon.android.model.TimelineDefinition;
import org.joinmastodon.android.ui.DividerItemDecoration;
@ -164,7 +167,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
makeBackItem(listsMenu);
makeBackItem(hashtagsMenu);
TimelineDefinition.ALL_TIMELINES.forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
TimelineDefinition.getAllTimelines(accountID).forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
@ -190,7 +193,7 @@ public class EditTimelinesFragment extends RecyclerFragment<TimelineDefinition>
@Override
protected void doLoadData(int offset, int count){
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES), false);
onDataLoaded(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)), false);
updateOptionsMenu();
}

View File

@ -83,6 +83,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
homeTabFragment=new HomeTabFragment();
homeTabFragment.setArguments(args);
args=new Bundle(args);
Instance instance = AccountSessionManager.getInstance().getAccount(accountID).getInstance();
args.putBoolean("isPleroma", instance.isPleroma());
args.putBoolean("noAutoLoad", true);
searchFragment=new DiscoverFragment();
searchFragment.setArguments(args);
@ -231,6 +233,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
if (newFragment instanceof HasFab fabulous) fabulous.showFab();
currentTab=tab;
((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this);
if (tab == R.id.tab_search)
searchFragment.selectSearch();
}
private void maybeTriggerLoading(Fragment newFragment){
@ -290,10 +294,10 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
public void updateNotificationBadge() {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
if (instance == null) return;
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.pleroma != null)
new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance != null && instance.isPleroma())
.setCallback(new Callback<>() {
@Override
public void onSuccess(List<Notification> notifications) {

View File

@ -106,7 +106,7 @@ public class HomeTabFragment extends MastodonToolbarFragment implements Scrollab
super.onCreate(savedInstanceState);
E.register(this);
accountID = getArguments().getString("account");
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES);
timelineDefinitions = GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID));
assert timelineDefinitions != null;
if (timelineDefinitions.size() == 0) timelineDefinitions = List.of(TimelineDefinition.HOME_TIMELINE);
count = timelineDefinitions.size();

View File

@ -9,7 +9,10 @@ import com.squareup.otto.Subscribe;
import org.joinmastodon.android.E;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonErrorResponse;
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
import org.joinmastodon.android.api.requests.notifications.PleromaMarkNotificationsRead;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.AllNotificationsSeenEvent;
import org.joinmastodon.android.events.PollUpdatedEvent;
@ -18,6 +21,7 @@ import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.CacheablePaginatedResponse;
import org.joinmastodon.android.model.Emoji;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Notification;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
@ -40,6 +44,7 @@ import java.util.stream.Stream;
import androidx.recyclerview.widget.RecyclerView;
import me.grishka.appkit.Nav;
import me.grishka.appkit.api.ErrorResponse;
import me.grishka.appkit.api.SimpleCallback;
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
@ -158,6 +163,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
AccountSessionManager.getInstance().getAccount(accountID).markers
.notifications.lastReadId = result.items.get(0).id;
AccountSessionManager.getInstance().writeAccountsFile();
if (AccountSessionManager.getInstance().getAccount(accountID).getInstance().isPleroma())
new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID);
}
}
});

View File

@ -20,7 +20,7 @@ public abstract class PinnableStatusListFragment extends StatusListFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.DEFAULT_TIMELINES));
pinnedTimelines = new ArrayList<>(GlobalUserPreferences.pinnedTimelines.getOrDefault(accountID, TimelineDefinition.getDefaultTimelines(accountID)));
}
@Override

View File

@ -31,7 +31,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;
@ -51,6 +50,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.account_list.FollowerListFragment;
import org.joinmastodon.android.fragments.account_list.FollowingListFragment;
@ -58,6 +58,7 @@ import org.joinmastodon.android.fragments.report.ReportReasonChoiceFragment;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.AccountField;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.model.Instance;
import org.joinmastodon.android.model.Relationship;
import org.joinmastodon.android.ui.BetterItemAnimator;
import org.joinmastodon.android.ui.SimpleViewHolder;
@ -137,6 +138,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private Account account;
private String accountID;
private String domain;
private Relationship relationship;
private int statusBarHeight;
private boolean isOwnProfile;
@ -151,7 +153,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private PhotoViewer currentPhotoViewer;
private boolean editModeLoading;
private static final int MAX_FIELDS=4;
private int maxFields = 4;
// from ProfileAboutFragment
public UsableRecyclerView list;
@ -172,6 +174,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
setRetainInstance(true);
accountID=getArguments().getString("account");
domain=AccountSessionManager.getInstance().getAccount(accountID).domain;
if(getArguments().containsKey("profileAccount")){
account=Parcels.unwrap(getArguments().getParcelable("profileAccount"));
profileAccountID=account.id;
@ -179,6 +182,12 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
loaded=true;
if(!isOwnProfile)
loadRelationship();
else {
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(domain);
if (instance.isPleroma()) {
maxFields = instance.pleroma.metadata.fieldsLimits.maxFields;
}
}
}else{
profileAccountID=getArguments().getString("profileAccountID");
if(!getArguments().getBoolean("noAutoLoad", false))
@ -324,7 +333,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setOnLongClickListener(v->{
String usernameString=account.acct;
if(!usernameString.contains("@")){
usernameString+="@"+AccountSessionManager.getInstance().getAccount(accountID).domain;
usernameString+="@"+domain;
}
UiUtils.copyText(username, '@'+usernameString);
return true;
@ -510,7 +519,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
ssb.append(account.acct);
if(isSelf){
ssb.append('@');
ssb.append(AccountSessionManager.getInstance().getAccount(accountID).domain);
ssb.append(domain);
}
ssb.append(" ");
Drawable lock=username.getResources().getDrawable(R.drawable.ic_lock, getActivity().getTheme()).mutate();
@ -520,7 +529,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
username.setText(ssb);
}else{
// noinspection SetTextI18n
username.setText('@'+account.acct+(isSelf ? ('@'+AccountSessionManager.getInstance().getAccount(accountID).domain) : ""));
username.setText('@'+account.acct+(isSelf ? ('@'+domain) : ""));
}
CharSequence parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID);
if(TextUtils.isEmpty(parsedBio)){
@ -1189,7 +1198,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
public int getItemCount(){
if(isInEditMode){
int size=metadataListData.size();
if(size<MAX_FIELDS)
if(size<maxFields)
size++;
return size;
}
@ -1306,7 +1315,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public void onClick(){
metadataListData.add(new AccountField());
if(metadataListData.size()==MAX_FIELDS){ // replace this row with new row
if(metadataListData.size()==maxFields){ // replace this row with new row
adapter.notifyItemChanged(metadataListData.size()-1);
}else{
adapter.notifyItemInserted(metadataListData.size()-1);

View File

@ -105,7 +105,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
imageCache = ImageCache.getInstance(getActivity());
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
String instanceName = UiUtils.getInstanceName(accountID);
if(GithubSelfUpdater.needSelfUpdating()){
@ -223,7 +223,7 @@ public class SettingsFragment extends MastodonToolbarFragment{
GlobalUserPreferences.showReplies=i.checked;
GlobalUserPreferences.save();
}));
if (instance.pleroma != null) {
if (instance.isPleroma()) {
items.add(new ButtonItem(R.string.sk_settings_reply_visibility, R.drawable.ic_fluent_chat_24_regular, b->{
PopupMenu popupMenu=new PopupMenu(getActivity(), b, Gravity.CENTER_HORIZONTAL);
popupMenu.inflate(R.menu.reply_visibility);

View File

@ -74,7 +74,7 @@ public class ThreadFragment extends StatusListFragment{
}
AccountSession account=AccountSessionManager.getInstance().getAccount(accountID);
Instance instance=AccountSessionManager.getInstance().getInstanceInfo(account.domain);
if(instance.pleroma != null){
if(instance.isPleroma()){
List<String> threadIds=new ArrayList<>();
threadIds.add(mainStatus.id);
for(Status s:result.descendants){

View File

@ -0,0 +1,55 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class BubbleTimelineFragment extends StatusListFragment {
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.BUBBLE_TIMELINE);
private String maxID;
@Override
protected boolean wantsComposeButton() {
return true;
}
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetBubbleTimeline(refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
if (getActivity() == null) return;
result=result.stream().filter(new StatusFilterPredicate(accountID, getFilterContext())).collect(Collectors.toList());
onDataLoaded(result, !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
@Override
protected Filter.FilterContext getFilterContext() {
return Filter.FilterContext.PUBLIC;
}
}

View File

@ -19,6 +19,7 @@ import android.widget.TextView;
import org.joinmastodon.android.GlobalUserPreferences;
import org.joinmastodon.android.R;
import org.joinmastodon.android.fragments.HomeFragment;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.ui.SimpleViewHolder;
@ -238,7 +239,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
else scrollToTop();
}
private void selectSearch() {
public void selectSearch() {
searchEdit.requestFocus();
onSearchEditFocusChanged(searchEdit, true);
getActivity().getSystemService(InputMethodManager.class).showSoftInput(searchEdit, 0);
@ -272,6 +273,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
searchBack.setEnabled(false);
searchBack.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
getActivity().getSystemService(InputMethodManager.class).hideSoftInputFromWindow(searchEdit.getWindowToken(), 0);
if (getArguments().getBoolean("isPleroma"))
((HomeFragment) getParentFragment()).onBackPressed();
}
@Override

View File

@ -1,5 +1,8 @@
package org.joinmastodon.android.fragments.discover;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withHistoryParams;
import static org.joinmastodon.android.ui.displayitems.HashtagStatusDisplayItem.Holder.withoutHistoryParams;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
@ -108,9 +111,11 @@ public class TrendingHashtagsFragment extends RecyclerFragment<Hashtag> implemen
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@ -86,6 +86,8 @@ public class Instance extends BaseModel{
public Pleroma pleroma;
public PleromaPollLimits pollLimits;
@Override
public void postprocess() throws ObjectValidationException{
super.postprocess();
@ -134,6 +136,10 @@ public class Instance extends BaseModel{
return ci;
}
public boolean isPleroma() {
return pleroma != null;
}
@Parcel
public static class Rule{
public String id;
@ -198,6 +204,28 @@ public class Instance extends BaseModel{
@Parcel
public static class Pleroma extends BaseModel {
// metadata etc
public Pleroma.Metadata metadata;
@Parcel
public static class Metadata {
public List<String> features;
public Pleroma.Metadata.FieldsLimits fieldsLimits;
@Parcel
public static class FieldsLimits {
public int maxFields;
public int maxRemoteFields;
public int nameLength;
public int valueLength;
}
}
}
@Parcel
public static class PleromaPollLimits {
public int maxExpiration;
public int maxOptionChars;
public int maxOptions;
public int minExpiration;
}
}

View File

@ -8,17 +8,20 @@ import androidx.annotation.DrawableRes;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import org.joinmastodon.android.BuildConfig;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
import org.joinmastodon.android.fragments.HomeTimelineFragment;
import org.joinmastodon.android.fragments.ListTimelineFragment;
import org.joinmastodon.android.fragments.NotificationsListFragment;
import org.joinmastodon.android.fragments.discover.BubbleTimelineFragment;
import org.joinmastodon.android.fragments.discover.FederatedTimelineFragment;
import org.joinmastodon.android.fragments.discover.LocalTimelineFragment;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
public class TimelineDefinition {
private TimelineType type;
@ -58,6 +61,14 @@ public class TimelineDefinition {
this.type = type;
}
public boolean isCompatible(AccountSession session) {
return true;
}
public boolean wantsDefault(AccountSession session) {
return true;
}
public String getTitle(Context ctx) {
return title != null ? title : getDefaultTitle(ctx);
}
@ -78,6 +89,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> ctx.getString(R.string.sk_timeline_posts);
case LIST -> listTitle;
case HASHTAG -> hashtagName;
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
};
}
@ -89,6 +101,7 @@ public class TimelineDefinition {
case POST_NOTIFICATIONS -> Icon.POST_NOTIFICATIONS;
case LIST -> Icon.LIST;
case HASHTAG -> Icon.HASHTAG;
case BUBBLE -> Icon.BUBBLE;
};
}
@ -100,6 +113,7 @@ public class TimelineDefinition {
case LIST -> new ListTimelineFragment();
case HASHTAG -> new HashtagTimelineFragment();
case POST_NOTIFICATIONS -> new NotificationsListFragment();
case BUBBLE -> new BubbleTimelineFragment();
};
}
@ -156,7 +170,7 @@ public class TimelineDefinition {
return args;
}
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG }
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE }
public enum Icon {
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
@ -219,7 +233,8 @@ public class TimelineDefinition {
FEDERATED(R.drawable.ic_fluent_earth_24_regular, R.string.sk_timeline_federated, true),
POST_NOTIFICATIONS(R.drawable.ic_fluent_chat_24_regular, R.string.sk_timeline_posts, true),
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true);
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
public final int iconRes, nameRes;
public final boolean hidden;
@ -239,14 +254,49 @@ public class TimelineDefinition {
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
@Override
public boolean isCompatible(AccountSession session) {
// still enabling the bubble timeline for all pleroma/akkoma instances since i know of
// at least one instance that supports it, but doesn't list "bubble_timeline"
return session.getInstance().isPleroma();
}
public static final List<TimelineDefinition> DEFAULT_TIMELINES = BuildConfig.BUILD_TYPE.equals("playRelease")
? List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy())
: List.of(HOME_TIMELINE.copy(), LOCAL_TIMELINE.copy(), FEDERATED_TIMELINE.copy());
public static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE.copy(),
LOCAL_TIMELINE.copy(),
FEDERATED_TIMELINE.copy(),
POSTS_TIMELINE.copy()
@Override
public boolean wantsDefault(AccountSession session) {
Instance instance = session.getInstance();
return instance.isPleroma() && instance.pleroma.metadata.features.contains("bubble_timeline");
}
};
public static List<TimelineDefinition> getDefaultTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return DEFAULT_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session) && tl.wantsDefault(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
public static List<TimelineDefinition> getAllTimelines(String accountId) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountId);
return ALL_TIMELINES.stream()
.filter(tl -> tl.isCompatible(session))
.map(TimelineDefinition::copy)
.collect(Collectors.toList());
}
private static final List<TimelineDefinition> DEFAULT_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
BUBBLE_TIMELINE,
FEDERATED_TIMELINE
);
private static final List<TimelineDefinition> ALL_TIMELINES = List.of(
HOME_TIMELINE,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POSTS_TIMELINE,
BUBBLE_TIMELINE
);
}

View File

@ -3,6 +3,7 @@ package org.joinmastodon.android.ui.displayitems;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RelativeLayout;
import android.widget.TextView;
import org.joinmastodon.android.R;
@ -26,6 +27,13 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
public static class Holder extends StatusDisplayItem.Holder<HashtagStatusDisplayItem>{
private final TextView title, subtitle;
private final HashtagChartView chart;
public static final RelativeLayout.LayoutParams
withHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT),
withoutHistoryParams = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
static {
withoutHistoryParams.addRule(RelativeLayout.CENTER_VERTICAL, RelativeLayout.TRUE);
}
public Holder(Context context, ViewGroup parent){
super(context, R.layout.item_trending_hashtag, parent);
@ -41,9 +49,11 @@ public class HashtagStatusDisplayItem extends StatusDisplayItem{
if (item.history == null || item.history.isEmpty()) {
subtitle.setText(null);
chart.setVisibility(View.GONE);
title.setLayoutParams(withoutHistoryParams);
return;
}
chart.setVisibility(View.VISIBLE);
title.setLayoutParams(withHistoryParams);
int numPeople=item.history.get(0).accounts;
if(item.history.size()>1)
numPeople+=item.history.get(1).accounts;

View File

@ -166,6 +166,15 @@ public abstract class StatusDisplayItem{
items.add(replyLine);
}
if (statusForContent.quote != null) {
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
if (!hasQuoteInlineTag) {
String quoteUrl = statusForContent.quote.url;
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
statusForContent.content += quoteInline;
}
}
if(!TextUtils.isEmpty(statusForContent.content))
items.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, disableTranslate));
else if (!GlobalUserPreferences.replyLineAboveHeader && replyLine != null)

View File

@ -38,6 +38,7 @@ public class DiscoverInfoBannerHelper{
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.sk_federated_timeline_info_banner;
case POST_NOTIFICATIONS -> R.string.sk_notify_posts_info_banner;
case BUBBLE_TIMELINE -> R.string.sk_bubble_timeline_info_banner;
});
}
}
@ -63,6 +64,7 @@ public class DiscoverInfoBannerHelper{
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
POST_NOTIFICATIONS,
// ACCOUNTS
// ACCOUNTS,
BUBBLE_TIMELINE
}
}

View File

@ -99,6 +99,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
@ -946,7 +947,7 @@ public class UiUtils {
public static String getInstanceName(String accountID) {
AccountSession session = AccountSessionManager.getInstance().getAccount(accountID);
Instance instance = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
Instance instance = session.getInstance();
return instance != null && !instance.title.isBlank() ? instance.title : session.domain;
}
@ -1112,14 +1113,20 @@ public class UiUtils {
if (!results.statuses.isEmpty()) {
args.putParcelable("status", Parcels.wrap(results.statuses.get(0)));
Nav.go((Activity) context, ThreadFragment.class, args);
} else if (!results.accounts.isEmpty()) {
args.putParcelable("profileAccount", Parcels.wrap(results.accounts.get(0)));
Nav.go((Activity) context, ProfileFragment.class, args);
} else {
if (launchBrowser) launchWebBrowser(context, url);
else
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
return;
}
Optional<Account> account = results.accounts.stream()
.filter(a -> uri.equals(Uri.parse(a.url))).findAny();
if (account.isPresent()) {
args.putParcelable("profileAccount", Parcels.wrap(account.get()));
Nav.go((Activity) context, ProfileFragment.class, args);
return;
}
if (launchBrowser) {
launchWebBrowser(context, url);
return;
}
Toast.makeText(context, R.string.sk_resource_not_found, Toast.LENGTH_SHORT).show();
}
@Override

View File

@ -30,6 +30,7 @@
<string name="sk_user_post_notifications_off">Turned off post notifications for %s</string>
<string name="sk_federated_timeline">Federation</string>
<string name="sk_federated_timeline_info_banner">These are the most recent posts by the people in your federation.</string>
<string name="sk_bubble_timeline_info_banner">These are the most recent posts by the people in your Akkoma server\'s bubble.</string>
<string name="sk_update_available">Megalodon %s is ready to download.</string>
<string name="sk_update_ready">Megalodon %s is downloaded and ready to install.</string>
<string name="sk_check_for_update">Check for update</string>
@ -148,6 +149,7 @@
<string name="sk_timeline_home">Home</string>
<string name="sk_timeline_local">Local</string>
<string name="sk_timeline_federated">Federation</string>
<string name="sk_timeline_bubble">Bubble</string>
<string name="sk_recent_searches_placeholder">Type to start searching</string>
<string name="sk_remove_follower">Remove as follower</string>
<string name="sk_remove_follower_confirm">Remove %s as a follower by blocking and immediately unblocking them?</string>