Follow Twitter/X tags through Nitter

This commit is contained in:
Thomas 2025-03-14 16:51:01 +01:00
parent 0d2ae2eedf
commit 88c18cc487
8 changed files with 54 additions and 17 deletions

View file

@ -212,6 +212,10 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ReorderTimelinesActivity.this); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(ReorderTimelinesActivity.this);
String nitterHost = sharedpreferences.getString(getString(R.string.SET_NITTER_HOST), getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase(); 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"; 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() OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS) .connectTimeout(10, TimeUnit.SECONDS)
@ -257,6 +261,8 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart
instanceType = RemoteInstance.InstanceType.GNU; instanceType = RemoteInstance.InstanceType.GNU;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) { } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) {
instanceType = RemoteInstance.InstanceType.NITTER; instanceType = RemoteInstance.InstanceType.NITTER;
} else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_tags) {
instanceType = RemoteInstance.InstanceType.NITTER_TAG;
} }
RemoteInstance remoteInstance = new RemoteInstance(); RemoteInstance remoteInstance = new RemoteInstance();
remoteInstance.type = instanceType; remoteInstance.type = instanceType;

View file

@ -46,6 +46,8 @@ public class RemoteInstance implements Serializable {
PEERTUBE("PEERTUBE"), PEERTUBE("PEERTUBE"),
@SerializedName("NITTER") @SerializedName("NITTER")
NITTER("NITTER"), NITTER("NITTER"),
@SerializedName("NITTER_TAG")
NITTER_TAG("NITTER_TAG"),
@SerializedName("MISSKEY") @SerializedName("MISSKEY")
MISSKEY("MISSKEY"), MISSKEY("MISSKEY"),
@SerializedName("LEMMY") @SerializedName("LEMMY")

View file

@ -341,10 +341,10 @@ public class PinnedTimelineHelper {
break; break;
case REMOTE: case REMOTE:
name = pinnedTimeline.remoteInstance.host; 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(); String remoteInstance = sharedpreferences.getString(activity.getString(R.string.SET_NITTER_HOST), activity.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
//Custom name for Nitter instances //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; name = pinnedTimeline.remoteInstance.displayName;
} }
ident = "@R@" + remoteInstance; ident = "@R@" + remoteInstance;
@ -378,6 +378,7 @@ public class PinnedTimelineHelper {
case MISSKEY: case MISSKEY:
tabCustomViewBinding.icon.setImageResource(R.drawable.misskey); tabCustomViewBinding.icon.setImageResource(R.drawable.misskey);
break; break;
case NITTER_TAG:
case NITTER: case NITTER:
tabCustomViewBinding.icon.setImageResource(R.drawable.nitter); tabCustomViewBinding.icon.setImageResource(R.drawable.nitter);
break; break;
@ -471,9 +472,10 @@ public class PinnedTimelineHelper {
case PIXELFED: case PIXELFED:
item.setIcon(R.drawable.pixelfed); item.setIcon(R.drawable.pixelfed);
break; break;
case NITTER_TAG:
case NITTER: case NITTER:
item.setIcon(R.drawable.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); item.setTitle(pinnedTimeline.remoteInstance.displayName);
} else { } else {
item.setTitle(pinnedTimeline.remoteInstance.host); item.setTitle(pinnedTimeline.remoteInstance.host);
@ -525,7 +527,7 @@ public class PinnedTimelineHelper {
bubbleClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); bubbleClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
break; break;
case REMOTE: 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()); instanceClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
} else { } else {
nitterClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString()); 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); PopupMenu popup = new PopupMenu(activity, view);
popup.getMenuInflater() popup.getMenuInflater()
.inflate(R.menu.option_nitter_timeline, popup.getMenu()); .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; int finalOffSetPosition = offSetPosition;
popup.setOnMenuItemClickListener(item -> { popup.setOnMenuItemClickListener(item -> {
int itemId = item.getItemId(); int itemId = item.getItemId();
@ -1547,7 +1555,7 @@ public class PinnedTimelineHelper {
} }
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> { dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
String values = editTextName.getText().toString(); String values = editTextName.getText().toString();
if (values.trim().length() == 0) { if (values.trim().isEmpty()) {
values = remoteInstance.displayName; values = remoteInstance.displayName;
} }
remoteInstance.displayName = values; remoteInstance.displayName = values;

View file

@ -100,14 +100,15 @@ public class ReorderTabAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
case GNU: case GNU:
holder.binding.icon.setImageResource(R.drawable.ic_gnu_social); holder.binding.icon.setImageResource(R.drawable.ic_gnu_social);
break; break;
case NITTER_TAG:
case NITTER: case NITTER:
holder.binding.icon.setImageResource(R.drawable.nitter); holder.binding.icon.setImageResource(R.drawable.nitter);
break; break;
} }
if (pinned.pinnedTimelines.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) { if (pinned.pinnedTimelines.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER && pinned.pinnedTimelines.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER_TAG) {
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host); holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host);
} else { } else {
if (pinned.pinnedTimelines.get(position).remoteInstance.displayName != null && pinned.pinnedTimelines.get(position).remoteInstance.displayName.trim().length() > 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); holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.displayName);
} else { } else {
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host); holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host);

View file

@ -420,7 +420,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
pinnedTimeline = (PinnedTimeline) bundle.getSerializable(Helper.ARG_REMOTE_INSTANCE); pinnedTimeline = (PinnedTimeline) bundle.getSerializable(Helper.ARG_REMOTE_INSTANCE);
canBeFederated = true; canBeFederated = true;
if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) { 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; remoteInstance = pinnedTimeline.remoteInstance.host;
} else { } else {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
@ -471,7 +471,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
} else if (list_id != null) { } else if (list_id != null) {
ident = "@l@" + list_id; ident = "@l@" + list_id;
} else if (remoteInstance != null && !checkRemotely) { } 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; ident = "@R@" + pinnedTimeline.remoteInstance.host;
} else { } else {
ident = "@R@" + remoteInstance; ident = "@R@" + remoteInstance;
@ -562,7 +562,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
} }
//Update the timeline with new statuses //Update the timeline with new statuses
int insertedStatus; 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(); insertedStatus = fetched_statuses.statuses.size();
int fromPosition = timelineStatuses.size(); int fromPosition = timelineStatuses.size();
timelineStatuses.addAll(fetched_statuses.statuses); timelineStatuses.addAll(fetched_statuses.statuses);
@ -685,7 +685,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
max_id = statuses.pagination.max_id; max_id = statuses.pagination.max_id;
} }
//For Lemmy and Nitter pagination //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; max_id = statuses.pagination.max_id;
} }
if (min_id == null || (statuses.pagination.min_id != null && Helper.compareTo(statuses.pagination.min_id, min_id) > 0)) { 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); routeCommon(direction, fetchingMissing, fetchStatus);
} else if (timelineType == Timeline.TimeLineEnum.REMOTE) { //REMOTE TIMELINE } else if (timelineType == Timeline.TimeLineEnum.REMOTE) { //REMOTE TIMELINE
//NITTER TIMELINES //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) { if (direction == null) {
timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.host, null) timelinesVM.getNitterHTML(pinnedTimeline.remoteInstance.type, pinnedTimeline.remoteInstance.host, null)
.observe(getViewLifecycleOwner(), nitterStatuses -> { .observe(getViewLifecycleOwner(), nitterStatuses -> {
initialStatuses = nitterStatuses; initialStatuses = nitterStatuses;
initializeStatusesCommonView(nitterStatuses); initializeStatusesCommonView(nitterStatuses);
}); });
} else if (direction == DIRECTION.BOTTOM) { } 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)); .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false, true, fetchStatus));
} else if (direction == DIRECTION.TOP) { } else if (direction == DIRECTION.TOP) {
flagLoading = false; flagLoading = false;
} else if (direction == DIRECTION.REFRESH || direction == DIRECTION.SCROLL_TOP) { } 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 -> { .observe(getViewLifecycleOwner(), statusesRefresh -> {
if (statusAdapter != null) { if (statusAdapter != null) {
dealWithPagination(statusesRefresh, direction, true, true, fetchStatus); dealWithPagination(statusesRefresh, direction, true, true, fetchStatus);

View file

@ -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.Statuses;
import app.fedilab.android.mastodon.client.entities.api.Tag; 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.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.StatusCache;
import app.fedilab.android.mastodon.client.entities.app.StatusDraft; import app.fedilab.android.mastodon.client.entities.app.StatusDraft;
import app.fedilab.android.mastodon.client.entities.app.Timeline; 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} * @return {@link LiveData} containing a {@link Statuses}
*/ */
public LiveData<Statuses> getNitterHTML( public LiveData<Statuses> getNitterHTML(
RemoteInstance.InstanceType instanceType,
String accountsStr, String accountsStr,
String max_position) { String max_position) {
statusesMutableLiveData = new MutableLiveData<>(); statusesMutableLiveData = new MutableLiveData<>();
@ -307,9 +309,20 @@ public class TimelinesVM extends AndroidViewModel {
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); 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 nitterInstance = sharedpreferences.getString(context.getString(R.string.SET_NITTER_HOST), context.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
final String fedilabInstance = "nitter.fedilab.app"; final String fedilabInstance = "nitter.fedilab.app";
accountsStr = accountsStr.replaceAll("\\s", ",").replaceAll(",,",",");
String cursor = max_position == null ? "" : max_position; 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() Request request = new Request.Builder()
.header("User-Agent", context.getString(R.string.app_name) + "/" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE) .header("User-Agent", context.getString(R.string.app_name) + "/" + BuildConfig.VERSION_NAME + "/" + BuildConfig.VERSION_CODE)
.url(url) .url(url)

View file

@ -65,6 +65,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/twitter_accounts" /> android:text="@string/twitter_accounts" />
<RadioButton
android:id="@+id/twitter_tags"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/twitter_tags" />
</RadioGroup> </RadioGroup>
</ScrollView> </ScrollView>
</LinearLayout> </LinearLayout>

View file

@ -618,6 +618,7 @@
<string name="set_utm_parameters_indication">The app will automatically remove UTM parameters from URLs before visiting a link.</string> <string name="set_utm_parameters_indication">The app will automatically remove UTM parameters from URLs before visiting a link.</string>
<string name="talking_about">%d people talking</string> <string name="talking_about">%d people talking</string>
<string name="twitter_accounts">Twitter accounts (via Nitter)</string> <string name="twitter_accounts">Twitter accounts (via Nitter)</string>
<string name="twitter_tags">Twitter tags (via Nitter)</string>
<string name="list_of_twitter_accounts">Twitter usernames space separated</string> <string name="list_of_twitter_accounts">Twitter usernames space separated</string>
<string name="identity_proofs">Identity proofs</string> <string name="identity_proofs">Identity proofs</string>
<string name="verified_user">Verified identity</string> <string name="verified_user">Verified identity</string>