diff options
35 files changed, 812 insertions, 42 deletions
diff --git a/app/build.gradle b/app/build.gradle index 6c334cbd6..faf618673 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { defaultConfig { minSdk 21 targetSdk 33 - versionCode 490 - versionName "3.22.0" + versionCode 491 + versionName "3.22.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1b8f4706a..9bf79691a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -266,6 +266,10 @@ android:label="@string/action_about" android:theme="@style/AppThemeBar" /> <activity + android:name=".mastodon.activities.TimelineActivity" + android:configChanges="keyboardHidden|orientation|screenSize" + android:theme="@style/AppThemeBar" /> + <activity android:name=".mastodon.activities.CheckHomeCacheActivity" android:configChanges="keyboardHidden|orientation|screenSize" android:label="@string/home_cache" diff --git a/app/src/main/assets/release_notes/notes.json b/app/src/main/assets/release_notes/notes.json index 291192b06..c6e74e1d3 100644 --- a/app/src/main/assets/release_notes/notes.json +++ b/app/src/main/assets/release_notes/notes.json @@ -1,5 +1,10 @@ [ { + "version": "3.22.1", + "code": "491", + "note": "Added:\n- Follow Lemmy instance (from Manage Timelines)\nFixed:\n- Add 50 chars max for poll options" + }, + { "version": "3.22.0", "code": "490", "note": "Fixed:\n- Too many requests\n- Blank Home page\n- Crashes when visiting profiles\n- Some audio files cannot be uploaded" diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/ContextActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/ContextActivity.java index 046792088..5c6579aa5 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/ContextActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/ContextActivity.java @@ -16,6 +16,7 @@ package app.fedilab.android.mastodon.activities; import static app.fedilab.android.BaseMainActivity.currentAccount; +import static app.fedilab.android.BaseMainActivity.currentInstance; import android.content.Intent; import android.content.SharedPreferences; @@ -60,6 +61,8 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon Fragment currentFragment; private Status firstMessage; private String remote_instance; + private Status focusedStatus; + private boolean checkRemotely; @Override protected void onCreate(Bundle savedInstanceState) { @@ -84,7 +87,7 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon } Bundle b = getIntent().getExtras(); displayCW = sharedpreferences.getBoolean(getString(R.string.SET_EXPAND_CW), false); - Status focusedStatus = null; // or other values + focusedStatus = null; // or other values if (b != null) { focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS); remote_instance = b.getString(Helper.ARG_REMOTE_INSTANCE, null); @@ -94,12 +97,24 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon return; } MastodonHelper.loadPPMastodon(binding.profilePicture, currentAccount.mastodon_account); + + checkRemotely = sharedpreferences.getBoolean(getString(R.string.SET_CONVERSATION_REMOTELY), false); + if (!checkRemotely) { + loadLocalConversation(); + } else { + loadRemotelyConversation(true); + invalidateOptionsMenu(); + } + } + + private void loadLocalConversation() { Bundle bundle = new Bundle(); bundle.putSerializable(Helper.ARG_STATUS, focusedStatus); bundle.putString(Helper.ARG_REMOTE_INSTANCE, remote_instance); FragmentMastodonContext fragmentMastodonContext = new FragmentMastodonContext(); fragmentMastodonContext.firstMessage = this; currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, fragmentMastodonContext, bundle, null, null); + //Update the status if (remote_instance == null) { StatusesVM timelinesVM = new ViewModelProvider(ContextActivity.this).get(StatusesVM.class); timelinesVM.getStatus(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, focusedStatus.id).observe(ContextActivity.this, status -> { @@ -126,7 +141,6 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon } } - @Override public boolean onCreateOptionsMenu(@NonNull Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. @@ -144,7 +158,7 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon itemDisplayCW.setIcon(R.drawable.ic_outline_remove_red_eye_24); } MenuItem action_remote = menu.findItem(R.id.action_remote); - if (remote_instance != null) { + if (remote_instance != null || checkRemotely) { action_remote.setVisible(false); } else { if (firstMessage != null && !firstMessage.visibility.equalsIgnoreCase("direct") && !firstMessage.visibility.equalsIgnoreCase("private")) { @@ -181,10 +195,62 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon } invalidateOptionsMenu(); } else if (item.getItemId() == R.id.action_remote) { + loadRemotelyConversation(false); + + } + return true; + } + private void loadRemotelyConversation(boolean fallback) { + if (fallback) { + StatusesVM statusesVM; + statusesVM = new ViewModelProvider(this).get(StatusesVM.class); + statusesVM.getContext(currentInstance, null, focusedStatus.id) + .observe(this, result -> { + if (result != null && result.ancestors != null && result.ancestors.size() > 0) { + firstMessage = result.ancestors.get(0); + String instance = null; + try { + URL url = new URL(firstMessage.uri); + instance = url.getHost(); + } catch (MalformedURLException e) { + e.printStackTrace(); + } + if (instance == null) { + loadLocalConversation(); + return; + } + Pattern pattern = Helper.statusIdInUrl; + Matcher matcher = pattern.matcher(firstMessage.uri); + String remoteId = null; + if (matcher.find()) { + remoteId = matcher.group(1); + } + if (remoteId == null) { + loadLocalConversation(); + return; + } + String finalInstance = instance; + statusesVM.getStatus(instance, null, remoteId).observe(ContextActivity.this, status -> { + if (status != null) { + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_STATUS, status); + bundle.putString(Helper.ARG_REMOTE_INSTANCE, finalInstance); + FragmentMastodonContext fragmentMastodonContext = new FragmentMastodonContext(); + fragmentMastodonContext.firstMessage = ContextActivity.this; + currentFragment = Helper.addFragment(getSupportFragmentManager(), R.id.nav_host_fragment_content_main, fragmentMastodonContext, bundle, null, null); + } else { + loadLocalConversation(); + } + }); + } else { + loadLocalConversation(); + } + }); + } else { if (firstMessage == null) { Toasty.warning(ContextActivity.this, getString(R.string.toast_try_later), Toasty.LENGTH_SHORT).show(); - return true; + return; } if (firstMessage.account.acct != null) { String instance = null; @@ -196,11 +262,11 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon } if (instance == null) { Toasty.info(ContextActivity.this, getString(R.string.toast_error_fetch_message), Toasty.LENGTH_SHORT).show(); - return true; + return; } if (instance.equalsIgnoreCase(MainActivity.currentInstance)) { Toasty.info(ContextActivity.this, getString(R.string.toast_on_your_instance), Toasty.LENGTH_SHORT).show(); - return true; + return; } Pattern pattern = Helper.statusIdInUrl; Matcher matcher = pattern.matcher(firstMessage.uri); @@ -229,7 +295,6 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon Toasty.warning(ContextActivity.this, getString(R.string.toast_error_fetch_message), Toasty.LENGTH_SHORT).show(); } } - return true; } @Override diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/ProfileActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/ProfileActivity.java index fc14056a1..90df4ff07 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/ProfileActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/ProfileActivity.java @@ -844,6 +844,8 @@ public class ProfileActivity extends BaseActivity { instanceType = RemoteInstance.InstanceType.PIXELFED; } else if (nodeInfo.software.name.compareToIgnoreCase("misskey") == 0) { instanceType = RemoteInstance.InstanceType.MISSKEY; + } else if (nodeInfo.software.name.compareToIgnoreCase("lemmy") == 0) { + instanceType = RemoteInstance.InstanceType.LEMMY; } else if (nodeInfo.software.name.compareToIgnoreCase("gnu") == 0) { instanceType = RemoteInstance.InstanceType.GNU; } else { 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 e25557e3b..5af29524a 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 @@ -203,6 +203,8 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) { url = "https://" + instanceName + "/api/notes/local-timeline"; getCall = false; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.lemmy_instance) { + url = "https://" + instanceName + "/api/v3/post/list"; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) { url = "https://" + instanceName + "/api/statuses/public_timeline.json"; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) { @@ -248,6 +250,8 @@ public class ReorderTimelinesActivity extends BaseBarActivity implements OnStart instanceType = RemoteInstance.InstanceType.PIXELFED; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.misskey_instance) { instanceType = RemoteInstance.InstanceType.MISSKEY; + } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.lemmy_instance) { + instanceType = RemoteInstance.InstanceType.LEMMY; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.gnu_instance) { instanceType = RemoteInstance.InstanceType.GNU; } else if (popupSearchInstanceBinding.setAttachmentGroup.getCheckedRadioButtonId() == R.id.twitter_accounts) { diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/TimelineActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/TimelineActivity.java new file mode 100644 index 000000000..6686f2073 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/TimelineActivity.java @@ -0,0 +1,83 @@ +package app.fedilab.android.mastodon.activities; +/* Copyright 2023 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see <http://www.gnu.org/licenses>. */ + +import android.os.Bundle; +import android.view.MenuItem; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.R; +import app.fedilab.android.databinding.ActivityTimelineBinding; +import app.fedilab.android.mastodon.client.entities.api.Status; +import app.fedilab.android.mastodon.client.entities.app.PinnedTimeline; +import app.fedilab.android.mastodon.client.entities.app.Timeline; +import app.fedilab.android.mastodon.helper.Helper; +import app.fedilab.android.mastodon.ui.fragment.timeline.FragmentMastodonTimeline; + + +public class TimelineActivity extends BaseBarActivity { + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + app.fedilab.android.databinding.ActivityTimelineBinding binding = ActivityTimelineBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + Bundle b = getIntent().getExtras(); + Timeline.TimeLineEnum timelineType = null; + String lemmy_post_id = null; + PinnedTimeline pinnedTimeline = null; + Status status = null; + if (b != null) { + timelineType = (Timeline.TimeLineEnum) b.get(Helper.ARG_TIMELINE_TYPE); + lemmy_post_id = b.getString(Helper.ARG_LEMMY_POST_ID, null); + pinnedTimeline = (PinnedTimeline) b.getSerializable(Helper.ARG_REMOTE_INSTANCE); + status = (Status) b.getSerializable(Helper.ARG_STATUS); + } + if (pinnedTimeline != null && pinnedTimeline.remoteInstance != null) { + setTitle(pinnedTimeline.remoteInstance.host); + } + FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, timelineType); + bundle.putSerializable(Helper.ARG_REMOTE_INSTANCE, pinnedTimeline); + bundle.putSerializable(Helper.ARG_LEMMY_POST_ID, lemmy_post_id); + if (status != null) { + bundle.putSerializable(Helper.ARG_STATUS, status); + } + fragmentMastodonTimeline.setArguments(bundle); + + getSupportFragmentManager().beginTransaction() + .add(R.id.fragment_container_view, fragmentMastodonTimeline).commit(); + } + + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + +} diff --git a/app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonTimelinesService.java index 3a014da05..6655c8c8d 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonTimelinesService.java +++ b/app/src/main/java/app/fedilab/android/mastodon/client/endpoints/MastodonTimelinesService.java @@ -22,6 +22,7 @@ import app.fedilab.android.mastodon.client.entities.api.Marker; import app.fedilab.android.mastodon.client.entities.api.MastodonList; import app.fedilab.android.mastodon.client.entities.api.Status; import app.fedilab.android.mastodon.client.entities.api.Tag; +import app.fedilab.android.mastodon.client.entities.lemmy.LemmyPost; import app.fedilab.android.mastodon.client.entities.misskey.MisskeyNote; import app.fedilab.android.mastodon.client.entities.nitter.Nitter; import app.fedilab.android.mastodon.client.entities.peertube.PeertubeVideo; @@ -230,6 +231,15 @@ public interface MastodonTimelinesService { Call<List<MisskeyNote>> getMisskey(@Body MisskeyNote.MisskeyParams params); + @GET("api/v3/post/list?sort=New") + Call<LemmyPost.LemmyPosts> getLemmyMain(@Query("limit") Integer limit, + @Query("page") String page); + + @GET("api/v3/comment/list") + Call<LemmyPost.LemmyComments> getLemmyThread(@Query("post_id") String post_id, + @Query("limit") Integer limit, + @Query("page") String page); + //Public timelines for Misskey @FormUrlEncoded @POST("api/notes") diff --git a/app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Status.java b/app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Status.java index e81d537f8..5dadc3812 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Status.java +++ b/app/src/main/java/app/fedilab/android/mastodon/client/entities/api/Status.java @@ -139,6 +139,7 @@ public class Status implements Serializable, Cloneable { public transient Spannable contentSpoilerSpan; public transient Spannable contentTranslateSpan; public transient MathJaxView mathJaxView; + public String lemmy_post_id; @Override public boolean equals(@Nullable Object obj) { 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 acec24b01..722b5f969 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 @@ -48,6 +48,8 @@ public class RemoteInstance implements Serializable { NITTER("NITTER"), @SerializedName("MISSKEY") MISSKEY("MISSKEY"), + @SerializedName("LEMMY") + LEMMY("LEMMY"), @SerializedName("GNU") GNU("GNU"); diff --git a/app/src/main/java/app/fedilab/android/mastodon/client/entities/lemmy/LemmyPost.java b/app/src/main/java/app/fedilab/android/mastodon/client/entities/lemmy/LemmyPost.java new file mode 100644 index 000000000..122e6e3bc --- /dev/null +++ b/app/src/main/java/app/fedilab/android/mastodon/client/entities/lemmy/LemmyPost.java @@ -0,0 +1,268 @@ +package app.fedilab.android.mastodon.client.entities.lemmy; +/* Copyright 2023 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see <http://www.gnu.org/licenses>. */ + +import com.google.gson.annotations.SerializedName; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import app.fedilab.android.mastodon.client.entities.api.Account; +import app.fedilab.android.mastodon.client.entities.api.Attachment; +import app.fedilab.android.mastodon.client.entities.api.Status; + +public class LemmyPost implements Serializable { + + @SerializedName("post") + public Post post; + @SerializedName("comment") + public Comment comment; + @SerializedName("creator") + public Creator creator; + /*@SerializedName("community") + public Community community;*/ + @SerializedName("counts") + public Counts counts; + @SerializedName("creator_banned_from_community") + public boolean creator_banned_from_community; + @SerializedName("saved") + public boolean saved; + @SerializedName("read") + public boolean read; + @SerializedName("creator_blocked") + public boolean creator_blocked; + @SerializedName("unread_comments") + public int unread_comments; + + public static class LemmyPosts { + @SerializedName("posts") + public List<LemmyPost> posts; + } + + public static class LemmyComments { + @SerializedName("comments") + public List<LemmyPost> comments; + } + + public static Status convert(LemmyPost lemmyPost, String instance) { + Status status = new Status(); + status.id = lemmyPost.comment == null ? lemmyPost.post.id : lemmyPost.comment.id; + if (lemmyPost.comment != null) { + status.in_reply_to_id = lemmyPost.comment.post_id; + status.lemmy_post_id = null; + } else { + status.lemmy_post_id = lemmyPost.post.id; + } + status.content = lemmyPost.comment == null ? lemmyPost.post.name : lemmyPost.comment.content; + status.visibility = "public"; + status.created_at = lemmyPost.comment == null ? lemmyPost.post.published : lemmyPost.comment.published; + status.url = lemmyPost.comment == null ? lemmyPost.post.ap_id : lemmyPost.comment.ap_id; + status.uri = lemmyPost.comment == null ? lemmyPost.post.ap_id : lemmyPost.comment.ap_id; + + + Account account = new Account(); + account.id = lemmyPost.creator.id; + account.acct = lemmyPost.creator.name + "@" + instance; + account.username = "@" + lemmyPost.creator.name; + account.display_name = lemmyPost.creator.name; + account.avatar = lemmyPost.creator.avatar; + account.avatar_static = lemmyPost.creator.avatar; + status.account = account; + + if (lemmyPost.comment == null && lemmyPost.post.thumbnail_url != null) { + List<Attachment> attachmentList = new ArrayList<>(); + Attachment attachment = new Attachment(); + attachment.type = "image"; + attachment.url = lemmyPost.post.thumbnail_url; + attachment.preview_url = lemmyPost.post.thumbnail_url; + if (lemmyPost.post.nsfw) { + status.sensitive = true; + } + attachmentList.add(attachment); + status.media_attachments = attachmentList; + } else if (lemmyPost.comment != null && lemmyPost.comment.thumbnail_url != null) { + List<Attachment> attachmentList = new ArrayList<>(); + Attachment attachment = new Attachment(); + attachment.type = "image"; + attachment.url = lemmyPost.comment.thumbnail_url; + attachment.preview_url = lemmyPost.comment.thumbnail_url; + if (lemmyPost.post.nsfw) { + status.sensitive = true; + } + attachmentList.add(attachment); + status.media_attachments = attachmentList; + } + return status; + } + + public static class Post implements Serializable { + @SerializedName("id") + public String id; + @SerializedName("name") + public String name; + @SerializedName("body") + public String body; + @SerializedName("creator_id") + public String creator_id; + @SerializedName("community_id") + public String community_id; + @SerializedName("remov |