From 88c18cc48700b44f494b04f202fe68214e227544 Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 14 Mar 2025 16:51:01 +0100 Subject: [PATCH] Follow Twitter/X tags through Nitter --- .../activities/ReorderTimelinesActivity.java | 6 ++++++ .../client/entities/app/RemoteInstance.java | 2 ++ .../mastodon/helper/PinnedTimelineHelper.java | 18 +++++++++++++----- .../mastodon/ui/drawer/ReorderTabAdapter.java | 5 +++-- .../timeline/FragmentMastodonTimeline.java | 16 ++++++++-------- .../viewmodel/mastodon/TimelinesVM.java | 17 +++++++++++++++-- .../mastodon/layout/popup_search_instance.xml | 6 ++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/ReorderTimelinesActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/ReorderTimelinesActivity.java index c611418f..06c69b6e 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/ReorderTimelinesActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/ReorderTimelinesActivity.java @@ -212,6 +212,10 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ReorderTimelinesActivity.this); String nitterHost = sharedpreferences.getString(getString(R.string.SET_NITTER_HOST), getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); url = "https://" + nitterHost + "/" + instanceName.replaceAll("[ ]+", ",").replaceAll("\\s", "") + "/with_replies/rss"; + }else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_tags) { + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ReorderTimelinesActivity.this); + String nitterHost = sharedpreferences.getString(getString(R.string.SET_NITTER_HOST), getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); + url = "https://" + nitterHost + "/search?f=tweets&q=" + instanceName.replaceAll("[ ]+", "+or+").replaceAll("\\s", "") + "&e-nativeretweets=on"; } OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) @@ -257,6 +261,8 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart instanceType = RemoteInstance.InstanceType.GNU; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) { instanceType = RemoteInstance.InstanceType.NITTER; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_tags) { + instanceType = RemoteInstance.InstanceType.NITTER_TAG; } RemoteInstance remoteInstance = new RemoteInstance(); remoteInstance.type = instanceType; diff --git a/app/src/main/java/app/fedilab/android/mastodon/client/entities/app/RemoteInstance.java b/app/src/main/java/app/fedilab/android/mastodon/client/entities/app/RemoteInstance.java index 722b5f96..1804b4a6 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/client/entities/app/RemoteInstance.java +++ b/app/src/main/java/app/fedilab/android/mastodon/client/entities/app/RemoteInstance.java @@ -46,6 +46,8 @@ public class RemoteInstance implements Serializable { PEERTUBE("PEERTUBE"), @SerializedName("NITTER") NITTER("NITTER"), + @SerializedName("NITTER_TAG") + NITTER_TAG("NITTER_TAG"), @SerializedName("MISSKEY") MISSKEY("MISSKEY"), @SerializedName("LEMMY") diff --git a/app/src/main/java/app/fedilab/android/mastodon/helper/PinnedTimelineHelper.java b/app/src/main/java/app/fedilab/android/mastodon/helper/PinnedTimelineHelper.java index 677b0086..13bae9f0 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/helper/PinnedTimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/mastodon/helper/PinnedTimelineHelper.java @@ -341,10 +341,10 @@ public class PinnedTimelineHelper { break; case REMOTE: name = pinnedTimeline.remoteInstance.host; - if (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { + if (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG) { String remoteInstance = sharedpreferences.getString(activity.getString(R.string.SET_NITTER_HOST), activity.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); //Custom name for Nitter instances - if (pinnedTimeline.remoteInstance.displayName != null && pinnedTimeline.remoteInstance.displayName.trim().length() > 0) { + if (pinnedTimeline.remoteInstance.displayName != null && !pinnedTimeline.remoteInstance.displayName.trim().isEmpty()) { name = pinnedTimeline.remoteInstance.displayName; } ident = "@R@" + remoteInstance; @@ -378,6 +378,7 @@ public class PinnedTimelineHelper { case MISSKEY: tabCustomViewBinding.icon.setImageResource(R.drawable.misskey); break; + case NITTER_TAG: case NITTER: tabCustomViewBinding.icon.setImageResource(R.drawable.nitter); break; @@ -471,9 +472,10 @@ public class PinnedTimelineHelper { case PIXELFED: item.setIcon(R.drawable.pixelfed); break; + case NITTER_TAG: case NITTER: item.setIcon(R.drawable.nitter); - if (pinnedTimeline.remoteInstance.displayName != null && pinnedTimeline.remoteInstance.displayName.trim().length() > 0) { + if (pinnedTimeline.remoteInstance.displayName != null && !pinnedTimeline.remoteInstance.displayName.trim().isEmpty()) { item.setTitle(pinnedTimeline.remoteInstance.displayName); } else { item.setTitle(pinnedTimeline.remoteInstance.host); @@ -525,7 +527,7 @@ public class PinnedTimelineHelper { bubbleClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); break; case REMOTE: - if (pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) { + if (pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER && pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER_TAG) { instanceClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); } else { nitterClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); @@ -1528,6 +1530,12 @@ public class PinnedTimelineHelper { PopupMenu popup = new PopupMenu(activity, view); popup.getMenuInflater() .inflate(R.menu.option_nitter_timeline, popup.getMenu()); + if(remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG) { + MenuItem item = popup.getMenu().findItem(R.id.action_nitter_manage_accounts); + if(item != null) { + item.setTitle(R.string.manage_tags); + } + } int finalOffSetPosition = offSetPosition; popup.setOnMenuItemClickListener(item -> { int itemId = item.getItemId(); @@ -1547,7 +1555,7 @@ public class PinnedTimelineHelper { } dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { String values = editTextName.getText().toString(); - if (values.trim().length() == 0) { + if (values.trim().isEmpty()) { values = remoteInstance.displayName; } remoteInstance.displayName = values; diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ReorderTabAdapter.java b/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ReorderTabAdapter.java index 8149a262..a7058e1e 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ReorderTabAdapter.java +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/drawer/ReorderTabAdapter.java @@ -100,14 +100,15 @@ public class ReorderTabAdapter extends RecyclerView.Adapter 0) { + if (pinned.pinnedTimelines.get(position).remoteInstance.displayName != null && !pinned.pinnedTimelines.get(position).remoteInstance.displayName.trim().isEmpty()) { holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.displayName); } else { holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host); diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java index 9f0fa760..e5040d3e 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -420,7 +420,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. pinnedTimeline = (PinnedTimeline) bundle.getSerializable(Helper.ARG_REMOTE_INSTANCE); canBeFederated = true; if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) { - if (pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER) { + if (pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER && pinnedTimeline.remoteInstance.type != RemoteInstance.InstanceType.NITTER_TAG) { remoteInstance = pinnedTimeline.remoteInstance.host; } else { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); @@ -471,7 +471,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else if (list_id != null) { ident = "@l@" + list_id; } else if (remoteInstance != null && !checkRemotely) { - if (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { + if (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG) { ident = "@R@" + pinnedTimeline.remoteInstance.host; } else { ident = "@R@" + remoteInstance; @@ -562,7 +562,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } //Update the timeline with new statuses int insertedStatus; - if(pinnedTimeline!= null && pinnedTimeline.remoteInstance != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { + if(pinnedTimeline!= null && pinnedTimeline.remoteInstance != null && (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG)) { insertedStatus = fetched_statuses.statuses.size(); int fromPosition = timelineStatuses.size(); timelineStatuses.addAll(fetched_statuses.statuses); @@ -685,7 +685,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. max_id = statuses.pagination.max_id; } //For Lemmy and Nitter pagination - if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null && (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER)) { + if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null && (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.LEMMY || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG)) { max_id = statuses.pagination.max_id; } if (min_id == null || (statuses.pagination.min_id != null && Helper.compareTo(statuses.pagination.min_id, min_id) > 0)) { @@ -1049,20 +1049,20 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. routeCommon(direction, fetchingMissing, fetchStatus); } else if (timelineType == Timeline.TimeLineEnum.REMOTE) { //REMOTE TIMELINE //NITTER TIMELINES - if (pinnedTimeline != null && pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) { + if (pinnedTimeline != null && (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER || pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER_TAG)) { if (direction == null) { - timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, null) + timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.type, pinnedTimeline.remoteInstance.host, null) .observe(getViewLifecycleOwner(), nitterStatuses -> { initialStatuses = nitterStatuses; initializeStatusesCommonView(nitterStatuses); }); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, max_id) + timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.type, pinnedTimeline.remoteInstance.host, max_id) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus)); } else if (direction == DIRECTION.TOP) { flagLoading = false; } else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { - timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, null) + timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.type, pinnedTimeline.remoteInstance.host, null) .observe(getViewLifecycleOwner(), statusesRefresh -> { if (statusAdapter != null) { dealWithPagination(statusesRefresh, direction, true, true, fetchStatus); diff --git a/app/src/main/java/app/fedilab/android/mastodon/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/mastodon/viewmodel/mastodon/TimelinesVM.java index ff4df4e2..3f1e4da6 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/viewmodel/mastodon/TimelinesVM.java +++ b/app/src/main/java/app/fedilab/android/mastodon/viewmodel/mastodon/TimelinesVM.java @@ -56,6 +56,7 @@ import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.client.entities.api.Statuses; import app.fedilab.android.mastodon.client.entities.api.Tag; import app.fedilab.android.mastodon.client.entities.app.BaseAccount; +import app.fedilab.android.mastodon.client.entities.app.RemoteInstance; import app.fedilab.android.mastodon.client.entities.app.StatusCache; import app.fedilab.android.mastodon.client.entities.app.StatusDraft; import app.fedilab.android.mastodon.client.entities.app.Timeline; @@ -300,6 +301,7 @@ public class TimelinesVM extends AndroidViewModel { * @return {@link LiveData} containing a {@link Statuses} */ public LiveData getNitterHTML( + RemoteInstance.InstanceType instanceType, String accountsStr, String max_position) { statusesMutableLiveData = new MutableLiveData<>(); @@ -307,9 +309,20 @@ public class TimelinesVM extends AndroidViewModel { SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); final String nitterInstance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); final String fedilabInstance = "nitter.fedilab.app"; - accountsStr = accountsStr.replaceAll("\\s", ",").replaceAll(",,",","); + String cursor = max_position == null ? "" : max_position; - String url = "https://"+fedilabInstance+"/" + accountsStr + "/with_replies" +cursor; + String url; + accountsStr = accountsStr.replaceAll("\\s", ",").replaceAll(",,",","); + if(instanceType == RemoteInstance.InstanceType.NITTER) { + url = "https://"+fedilabInstance+"/" + accountsStr + "/with_replies" +cursor; + } else { + String[] tags = accountsStr.split(","); + StringBuilder tagsQuery = new StringBuilder(); + for(String tag: tags) { + tagsQuery.append("%23").append(tag).append("+or+"); + } + url = "https://"+fedilabInstance+"/search?f=tweets&q=" + tagsQuery +"&e-nativeretweets=on&" +cursor; + } Request request = new Request.Builder() .header("User-Agent", context.getString(R.string.app_name) + "/" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE) .url(url) diff --git a/app/src/main/res/layouts/mastodon/layout/popup_search_instance.xml b/app/src/main/res/layouts/mastodon/layout/popup_search_instance.xml index 4a09646a..d4ea4eb4 100644 --- a/app/src/main/res/layouts/mastodon/layout/popup_search_instance.xml +++ b/app/src/main/res/layouts/mastodon/layout/popup_search_instance.xml @@ -65,6 +65,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/twitter_accounts" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 420c7bc7..c5680bb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -618,6 +618,7 @@ The app will automatically remove UTM parameters from URLs before visiting a link. %d people talking Twitter accounts (via Nitter) + Twitter tags (via Nitter) Twitter usernames space separated Identity proofs Verified identity